Add TypeScript Agent SDK reference documentation

- Introduced comprehensive API reference for the TypeScript Agent SDK.
- Documented installation instructions for the SDK.
- Detailed the main functions: `query()`, `tool()`, and `createSdkMcpServer()`.
- Defined various types including `Options`, `Query`, `AgentDefinition`, and more.
- Included message types and their structures, such as `SDKMessage`, `SDKAssistantMessage`, and `SDKUserMessage`.
- Explained hook types and their usage within the SDK.
- Provided detailed documentation for tool input and output types.
- Added sections on permission types and other relevant types for better clarity.
This commit is contained in:
Alex Newman
2025-11-07 15:05:31 -05:00
parent 4bc467f7ed
commit 740d65b5a5
14 changed files with 2141 additions and 161 deletions
-4
View File
@@ -6,7 +6,6 @@
import path from 'path';
import { stdin } from 'process';
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { ensureWorkerRunning } from '../shared/worker-utils.js';
// Configuration: Read from environment or use defaults
const DISPLAY_OBSERVATION_COUNT = parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS || '50', 10);
@@ -127,9 +126,6 @@ function getObservations(db: SessionStore, sessionIds: string[]): Observation[]
* Context Hook Main Logic
*/
async function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): Promise<string> {
// Ensure worker is running
await ensureWorkerRunning();
const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : 'unknown-project';
+6
View File
@@ -68,6 +68,12 @@ async function summaryHook(input?: StopInput): Promise<void> {
}
// Re-throw HTTP errors and other errors as-is
throw error;
} finally {
await fetch(`http://127.0.0.1:${port}/api/processing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isProcessing: false })
});
}
console.log(createHookResponse('Stop', true));
+1
View File
@@ -569,6 +569,7 @@ class WorkerService {
ss.id,
s.claude_session_id as session_id,
ss.request,
ss.investigated,
ss.learned,
ss.completed,
ss.next_steps,
+35 -18
View File
@@ -43,7 +43,6 @@ export class WorkerService {
// Processing status tracking for viewer UI spinner
private isProcessing: boolean = false;
private spinnerStopTimer: NodeJS.Timeout | null = null;
constructor() {
this.app = express();
@@ -96,6 +95,7 @@ export class WorkerService {
this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
this.app.get('/api/stats', this.handleGetStats.bind(this));
this.app.get('/api/processing-status', this.handleGetProcessingStatus.bind(this));
this.app.post('/api/processing', this.handleSetProcessing.bind(this));
// Settings
this.app.get('/api/settings', this.handleGetSettings.bind(this));
@@ -266,6 +266,7 @@ export class WorkerService {
/**
* Queue observations for processing
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
*/
private handleObservations(req: Request, res: Response): void {
try {
@@ -279,6 +280,14 @@ export class WorkerService {
prompt_number
});
// CRITICAL: Ensure SDK agent is running to consume the queue
const session = this.sessionManager.getSession(sessionDbId);
if (session && !session.generatorPromise) {
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
});
}
// Broadcast SSE event
this.sseBroadcaster.broadcast({
type: 'observation_queued',
@@ -294,12 +303,21 @@ export class WorkerService {
/**
* Queue summarize request
* CRITICAL: Ensures SDK agent is running to process the queue (ALWAYS SAVE EVERYTHING)
*/
private handleSummarize(req: Request, res: Response): void {
try {
const sessionDbId = parseInt(req.params.sessionDbId, 10);
this.sessionManager.queueSummarize(sessionDbId);
// CRITICAL: Ensure SDK agent is running to consume the queue
const session = this.sessionManager.getSession(sessionDbId);
if (session && !session.generatorPromise) {
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
});
}
res.json({ status: 'queued' });
} catch (error) {
logger.failure('WORKER', 'Summarize queuing failed', {}, error as Error);
@@ -604,25 +622,24 @@ export class WorkerService {
}
/**
* Check if all sessions have empty queues and stop spinner after debounce (1.5s)
* Set processing status (called by hooks)
*/
checkAndStopSpinner(): void {
// Clear any existing timer
if (this.spinnerStopTimer) {
clearTimeout(this.spinnerStopTimer);
this.spinnerStopTimer = null;
}
private handleSetProcessing(req: Request, res: Response): void {
try {
const { isProcessing } = req.body;
// Check if any session has pending messages
if (!this.sessionManager.hasPendingMessages()) {
// Debounce: wait 1.5s and check again
this.spinnerStopTimer = setTimeout(() => {
if (!this.sessionManager.hasPendingMessages()) {
logger.debug('WORKER', 'All queues empty - stopping spinner');
this.broadcastProcessingStatus(false);
}
this.spinnerStopTimer = null;
}, 1500);
if (typeof isProcessing !== 'boolean') {
res.status(400).json({ error: 'isProcessing must be a boolean' });
return;
}
this.broadcastProcessingStatus(isProcessing);
logger.debug('WORKER', 'Processing status updated', { isProcessing });
res.json({ status: 'ok', isProcessing });
} catch (error) {
logger.failure('WORKER', 'Failed to set processing status', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
}
+58 -1
View File
@@ -17,17 +17,73 @@ export class PaginationHelper {
this.dbManager = dbManager;
}
/**
* Strip project path from file paths using heuristic
* Converts "/Users/user/project/src/file.ts" -> "src/file.ts"
* Uses first occurrence of project name from left (project root)
*/
private stripProjectPath(filePath: string, projectName: string): string {
const marker = `/${projectName}/`;
const index = filePath.indexOf(marker);
if (index !== -1) {
// Strip everything before and including the project name
return filePath.substring(index + marker.length);
}
// Fallback: return original path if project name not found
return filePath;
}
/**
* Strip project path from JSON array of file paths
*/
private stripProjectPaths(filePathsStr: string | null, projectName: string): string | null {
if (!filePathsStr) return filePathsStr;
try {
// Parse JSON array
const paths = JSON.parse(filePathsStr) as string[];
// Strip project path from each file
const strippedPaths = paths.map(p => this.stripProjectPath(p, projectName));
// Return as JSON string
return JSON.stringify(strippedPaths);
} catch (error) {
// If parsing fails, return original string
return filePathsStr;
}
}
/**
* Sanitize observation by stripping project paths from files
*/
private sanitizeObservation(obs: Observation): Observation {
return {
...obs,
files_read: this.stripProjectPaths(obs.files_read, obs.project),
files_modified: this.stripProjectPaths(obs.files_modified, obs.project)
};
}
/**
* Get paginated observations
*/
getObservations(offset: number, limit: number, project?: string): PaginatedResult<Observation> {
return this.paginate<Observation>(
const result = this.paginate<Observation>(
'observations',
'id, sdk_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch',
offset,
limit,
project
);
// Strip project paths from file paths before returning
return {
...result,
items: result.items.map(obs => this.sanitizeObservation(obs))
};
}
/**
@@ -41,6 +97,7 @@ export class PaginationHelper {
ss.id,
s.claude_session_id as session_id,
ss.request,
ss.investigated,
ss.learned,
ss.completed,
ss.next_steps,
+44 -2
View File
@@ -578,8 +578,8 @@
color: var(--color-text-primary);
}
/* Expanded content container */
.card-expanded-content {
/* Verbose content container (narrative and facts - collapsible) */
.card-verbose-content {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-primary);
@@ -597,6 +597,48 @@
}
}
/* Metadata grid (concepts, files, session info - always visible) */
.card-metadata-grid {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-primary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
grid-auto-flow: dense;
}
/* Files section spans full width */
.card-metadata-grid .card-section.files-section {
grid-column: 1 / -1;
}
/* Responsive: stack on smaller screens */
@media (max-width: 768px) {
.card-metadata-grid {
grid-template-columns: 1fr;
}
.card-metadata-grid .card-section.files-section {
grid-column: 1;
}
}
/* Compact section styling for grid items */
.card-section.compact {
margin-bottom: 0;
}
.card-section.compact .section-header {
font-size: 12px;
margin-bottom: 6px;
}
.card-section.compact .section-content {
padding-left: 0;
font-size: 12px;
}
/* Section styling */
.card-section {
margin-bottom: 16px;
+90 -60
View File
@@ -6,6 +6,30 @@ interface ObservationCardProps {
observation: Observation;
}
// Helper to strip project root from file paths
function stripProjectRoot(filePath: string): string {
// Try to extract relative path by finding common project markers
const markers = ['/Scripts/', '/src/', '/plugin/', '/docs/'];
for (const marker of markers) {
const index = filePath.indexOf(marker);
if (index !== -1) {
// Keep the marker and everything after it
return filePath.substring(index + 1);
}
}
// Fallback: if path contains project name, strip everything before it
const projectIndex = filePath.indexOf('claude-mem/');
if (projectIndex !== -1) {
return filePath.substring(projectIndex + 'claude-mem/'.length);
}
// If no markers found, return basename or original path
const parts = filePath.split('/');
return parts.length > 3 ? parts.slice(-3).join('/') : filePath;
}
export function ObservationCard({ observation }: ObservationCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
const date = formatDate(observation.created_at_epoch);
@@ -13,11 +37,14 @@ export function ObservationCard({ observation }: ObservationCardProps) {
// Parse JSON fields
const facts = observation.facts ? JSON.parse(observation.facts) : [];
const concepts = observation.concepts ? JSON.parse(observation.concepts) : [];
const filesRead = observation.files_read ? JSON.parse(observation.files_read) : [];
const filesModified = observation.files_modified ? JSON.parse(observation.files_modified) : [];
const filesRead = observation.files_read ? JSON.parse(observation.files_read).map(stripProjectRoot) : [];
const filesModified = observation.files_modified ? JSON.parse(observation.files_modified).map(stripProjectRoot) : [];
// Check if there's verbose content to expand
const hasVerboseContent = observation.narrative || facts.length > 0;
return (
<div className={`card ${isExpanded ? 'card-expanded' : ''}`}>
<div className="card">
{/* Header - always visible */}
<div className="card-header">
<span className={`card-type type-${observation.type}`}>
@@ -35,18 +62,19 @@ export function ObservationCard({ observation }: ObservationCardProps) {
{/* Metadata + Expand button - always visible */}
<div className="card-meta">
<span>#{observation.id} {date}</span>
<button
className="expand-toggle"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '▲ Less' : '▼ More'}
</button>
{hasVerboseContent && (
<button
className="expand-toggle"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? '▲ Less' : '▼ More'}
</button>
)}
</div>
{/* Expanded content - conditional */}
{isExpanded && (
<div className="card-expanded-content">
{/* Collapsible verbose content - Narrative and Facts */}
{isExpanded && hasVerboseContent && (
<div className="card-verbose-content">
{/* Narrative Section */}
{observation.narrative && (
<div className="card-section">
@@ -68,61 +96,63 @@ export function ObservationCard({ observation }: ObservationCardProps) {
</ul>
</div>
)}
</div>
)}
{/* Concepts Section */}
{concepts.length > 0 && (
<div className="card-section">
<div className="section-header">🏷 Concepts</div>
<div className="section-content concepts">
{concepts.map((concept: string, i: number) => (
<span key={i} className="concept-tag">{concept}</span>
))}
</div>
{/* Always visible metadata grid - Concepts, Files, Session Info */}
<div className="card-metadata-grid">
{/* Concepts Section */}
{concepts.length > 0 && (
<div className="card-section compact">
<div className="section-header">Concepts</div>
<div className="section-content concepts">
{concepts.map((concept: string, i: number) => (
<span key={i} className="concept-tag">{concept}</span>
))}
</div>
)}
</div>
)}
{/* Files Section */}
{(filesRead.length > 0 || filesModified.length > 0) && (
<div className="card-section">
<div className="section-header">📁 Files</div>
<div className="section-content files">
{filesRead.length > 0 && (
<div className="file-group">
<div className="file-group-label">📖 Read:</div>
{filesRead.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
{filesModified.length > 0 && (
<div className="file-group">
<div className="file-group-label"> Modified:</div>
{filesModified.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
</div>
</div>
)}
{/* Session Info Section */}
<div className="card-section compact">
<div className="section-header">Session Info</div>
<div className="section-content session-info">
{observation.prompt_number && (
<span>Prompt #{observation.prompt_number}</span>
)}
{observation.sdk_session_id && (
<span className="session-id">
Session: {observation.sdk_session_id.substring(0, 8)}...
</span>
)}
</div>
</div>
{/* Session Info Section */}
<div className="card-section">
<div className="section-header">🔗 Session Info</div>
<div className="section-content session-info">
{observation.prompt_number && (
<span>Prompt #{observation.prompt_number}</span>
{/* Files Section - spans full width */}
{(filesRead.length > 0 || filesModified.length > 0) && (
<div className="card-section compact files-section">
<div className="section-header">Files</div>
<div className="section-content files">
{filesRead.length > 0 && (
<div className="file-group">
<div className="file-group-label">Read:</div>
{filesRead.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
{observation.sdk_session_id && (
<span className="session-id">
Session: {observation.sdk_session_id.substring(0, 8)}...
</span>
{filesModified.length > 0 && (
<div className="file-group">
<div className="file-group-label">Modified:</div>
{filesModified.map((file: string, i: number) => (
<div key={i} className="file-path">{file}</div>
))}
</div>
)}
</div>
</div>
</div>
)}
)}
</div>
</div>
);
}