Skip to content

Commit 8a460f3

Browse files
author
StackMemory Bot (CLI)
committed
fix: codex-sm linear sync, db enhancements, and hook installer updates
- Remove LINEAR_API_KEY env guard from codex-sm exit sync — let sync command handle auth detection (supports env var, .env, and OAuth) - Enhance sqlite-adapter and frame-database with additional queries - Update linear sync engine with improved error handling - Update hook installer with new capabilities - Bump dependencies in package.json
1 parent aa11d6c commit 8a460f3

10 files changed

Lines changed: 205 additions & 20 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackmemoryai/stackmemory",
3-
"version": "1.2.3",
3+
"version": "1.2.4",
44
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.",
55
"engines": {
66
"node": ">=20.0.0",
@@ -121,6 +121,7 @@
121121
"daemon:status": "node dist/cli/index.js daemon status",
122122
"sync:start": "node scripts/background-sync-manager.js",
123123
"sync:setup": "./scripts/setup-background-sync.sh",
124+
"eval:cord": "npx tsx scripts/evals/cord-vs-flat-eval.ts",
124125
"prepare": "echo 'Prepare step completed'",
125126
"verify:dist": "node scripts/verify-dist.cjs",
126127
"test:smoke-db": "bash scripts/smoke-init-db.sh",

scripts/install-claude-hooks-auto.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ try {
8181
commandPrefix: 'node',
8282
required: true,
8383
},
84+
{
85+
scriptName: 'cord-trace.js',
86+
eventType: 'PostToolUse',
87+
matcher: 'mcp__.*__cord_(spawn|fork|complete|ask|tree)',
88+
timeout: 2,
89+
commandPrefix: 'node',
90+
required: true,
91+
},
8492
];
8593

8694
const DEAD_HOOKS = ['sms-response-handler.js'];

src/cli/codex-sm.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -407,16 +407,15 @@ class CodexSM {
407407
exitCode: code,
408408
});
409409

410-
// Sync Linear on exit if configured
411-
if (process.env['LINEAR_API_KEY']) {
412-
try {
413-
execSync('stackmemory linear sync', {
414-
stdio: 'ignore',
415-
timeout: 10000,
416-
});
417-
} catch {
418-
// Non-fatal: don't block exit
419-
}
410+
// Sync Linear on exit — let sync command handle auth detection
411+
// (supports API key env var, .env files, and OAuth tokens)
412+
try {
413+
execSync('stackmemory linear sync', {
414+
stdio: 'ignore',
415+
timeout: 10000,
416+
});
417+
} catch {
418+
// Non-fatal: don't block exit
420419
}
421420

422421
if (this.config.tracingEnabled) {

src/cli/commands/linear.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,16 @@ export function registerLinearCommands(parent: Command) {
387387
config
388388
);
389389

390+
if (!syncEngine.isConfigured) {
391+
console.log(
392+
chalk.gray(
393+
'ℹ Linear API key not configured — skipping sync. Set LINEAR_API_KEY or run "stackmemory linear setup".'
394+
)
395+
);
396+
db.close();
397+
return;
398+
}
399+
390400
console.log(chalk.yellow('🔄 Syncing with Linear...'));
391401

392402
if (options.dryRun) {

src/core/context/frame-database.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,32 @@ export class FrameDatabase {
227227
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
228228
`);
229229

230+
// Cord task orchestration table (dependency DAG for multi-agent coordination)
231+
this.db.exec(`
232+
CREATE TABLE IF NOT EXISTS cord_tasks (
233+
task_id TEXT PRIMARY KEY,
234+
parent_id TEXT,
235+
project_id TEXT NOT NULL,
236+
run_id TEXT NOT NULL,
237+
goal TEXT NOT NULL,
238+
prompt TEXT NOT NULL DEFAULT '',
239+
result TEXT,
240+
status TEXT NOT NULL DEFAULT 'pending'
241+
CHECK (status IN ('pending','active','completed','blocked','asked')),
242+
context_mode TEXT NOT NULL DEFAULT 'spawn'
243+
CHECK (context_mode IN ('spawn','fork','ask')),
244+
blocked_by TEXT NOT NULL DEFAULT '[]',
245+
depth INTEGER NOT NULL DEFAULT 0,
246+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
247+
completed_at INTEGER,
248+
FOREIGN KEY (parent_id) REFERENCES cord_tasks(task_id)
249+
);
250+
CREATE INDEX IF NOT EXISTS idx_cord_tasks_project ON cord_tasks(project_id);
251+
CREATE INDEX IF NOT EXISTS idx_cord_tasks_parent ON cord_tasks(parent_id);
252+
CREATE INDEX IF NOT EXISTS idx_cord_tasks_status ON cord_tasks(status);
253+
CREATE INDEX IF NOT EXISTS idx_cord_tasks_project_status ON cord_tasks(project_id, status);
254+
`);
255+
230256
logger.info('Frame database schema initialized');
231257
} catch (error: unknown) {
232258
throw new DatabaseError(

src/core/database/sqlite-adapter.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,122 @@ export class SQLiteAdapter extends FeatureAwareDatabaseAdapter {
19271927
return Buffer.from(JSON.stringify(data, null, 2));
19281928
}
19291929

1930+
/**
1931+
* Get recent frames from other run_ids within the same project.
1932+
* Used by team tools to read cross-agent context.
1933+
*/
1934+
async getRecentFramesExcludingRun(
1935+
projectId: string,
1936+
excludeRunId: string,
1937+
opts?: { limit?: number; since?: number; types?: string[] }
1938+
): Promise<Array<Frame & { anchors: Anchor[] }>> {
1939+
if (!this.db)
1940+
throw new DatabaseError(
1941+
'Database not connected',
1942+
ErrorCode.DB_CONNECTION_FAILED
1943+
);
1944+
1945+
const limit = opts?.limit ?? 10;
1946+
const params: any[] = [projectId, excludeRunId];
1947+
let whereExtra = '';
1948+
1949+
if (opts?.since) {
1950+
whereExtra += ' AND created_at > ?';
1951+
params.push(Math.floor(opts.since / 1000));
1952+
}
1953+
1954+
if (opts?.types && opts.types.length > 0) {
1955+
const placeholders = opts.types.map(() => '?').join(',');
1956+
whereExtra += ` AND type IN (${placeholders})`;
1957+
params.push(...opts.types);
1958+
}
1959+
1960+
params.push(limit);
1961+
1962+
const rows = this.db
1963+
.prepare(
1964+
`SELECT * FROM frames
1965+
WHERE project_id = ? AND run_id != ?${whereExtra}
1966+
ORDER BY created_at DESC
1967+
LIMIT ?`
1968+
)
1969+
.all(...params) as FrameRow[];
1970+
1971+
// Batch-fetch anchors for all returned frames
1972+
const frameIds = rows.map((r) => r.frame_id);
1973+
const anchorsByFrame = new Map<string, Anchor[]>();
1974+
1975+
if (frameIds.length > 0) {
1976+
const placeholders = frameIds.map(() => '?').join(',');
1977+
const anchorRows = this.db
1978+
.prepare(
1979+
`SELECT * FROM anchors WHERE frame_id IN (${placeholders})
1980+
ORDER BY priority DESC, created_at ASC`
1981+
)
1982+
.all(...frameIds) as AnchorRow[];
1983+
1984+
for (const row of anchorRows) {
1985+
const list = anchorsByFrame.get(row.frame_id) || [];
1986+
list.push({ ...row, metadata: JSON.parse(row.metadata || '{}') });
1987+
anchorsByFrame.set(row.frame_id, list);
1988+
}
1989+
}
1990+
1991+
return rows.map((row) => ({
1992+
...row,
1993+
inputs: JSON.parse(row.inputs || '{}'),
1994+
outputs: JSON.parse(row.outputs || '{}'),
1995+
digest_json: JSON.parse(row.digest_json || '{}'),
1996+
anchors: anchorsByFrame.get(row.frame_id) || [],
1997+
}));
1998+
}
1999+
2000+
/**
2001+
* Get anchors explicitly shared for team visibility.
2002+
* Finds anchors where metadata contains `"shared":true`.
2003+
*/
2004+
async getSharedAnchors(
2005+
projectId: string,
2006+
opts?: { limit?: number; since?: number }
2007+
): Promise<Array<Anchor & { frame_name?: string; run_id?: string }>> {
2008+
if (!this.db)
2009+
throw new DatabaseError(
2010+
'Database not connected',
2011+
ErrorCode.DB_CONNECTION_FAILED
2012+
);
2013+
2014+
const limit = opts?.limit ?? 20;
2015+
const params: any[] = [projectId];
2016+
let whereExtra = '';
2017+
2018+
if (opts?.since) {
2019+
whereExtra += ' AND a.created_at > ?';
2020+
params.push(Math.floor(opts.since / 1000));
2021+
}
2022+
2023+
params.push(limit);
2024+
2025+
const rows = this.db
2026+
.prepare(
2027+
`SELECT a.*, f.name as frame_name, f.run_id
2028+
FROM anchors a
2029+
JOIN frames f ON a.frame_id = f.frame_id
2030+
WHERE f.project_id = ?
2031+
AND a.metadata LIKE '%"shared":true%'${whereExtra}
2032+
ORDER BY a.priority DESC, a.created_at DESC
2033+
LIMIT ?`
2034+
)
2035+
.all(...params) as (AnchorRow & {
2036+
frame_name?: string;
2037+
run_id?: string;
2038+
})[];
2039+
2040+
return rows.map((row) => ({
2041+
...row,
2042+
metadata: JSON.parse(row.metadata || '{}'),
2043+
}));
2044+
}
2045+
19302046
async importData(
19312047
data: Buffer,
19322048
format: 'json' | 'parquet' | 'csv',

src/integrations/linear/sync.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ export interface TaskMapping {
5858

5959
export class LinearSyncEngine {
6060
private taskStore: LinearTaskManager;
61-
private linearClient: LinearClient;
61+
private linearClient!: LinearClient;
6262
private authManager: LinearAuthManager;
6363
private config: SyncConfig;
6464
private mappings: Map<string, TaskMapping> = new Map();
6565
private projectRoot: string;
6666
private mappingsPath: string;
67+
private _isConfigured: boolean = true;
6768

6869
constructor(
6970
taskStore: LinearTaskManager,
@@ -93,10 +94,9 @@ export class LinearSyncEngine {
9394
// Fall back to OAuth tokens
9495
const tokens = this.authManager.loadTokens();
9596
if (!tokens) {
96-
throw new IntegrationError(
97-
'Linear API key or authentication tokens not found. Set LINEAR_API_KEY environment variable or run "stackmemory linear setup" first.',
98-
ErrorCode.LINEAR_SYNC_FAILED
99-
);
97+
logger.info('Linear API key not configured — skipping Linear sync');
98+
this._isConfigured = false;
99+
return;
100100
}
101101

102102
this.linearClient = new LinearClient({
@@ -112,6 +112,13 @@ export class LinearSyncEngine {
112112
this.loadMappings();
113113
}
114114

115+
/**
116+
* Check if Linear credentials are available
117+
*/
118+
get isConfigured(): boolean {
119+
return this._isConfigured;
120+
}
121+
115122
/**
116123
* Update sync configuration
117124
*/
@@ -123,6 +130,15 @@ export class LinearSyncEngine {
123130
* Perform bi-directional sync
124131
*/
125132
async sync(): Promise<SyncResult> {
133+
if (!this._isConfigured) {
134+
return {
135+
success: true,
136+
synced: { toLinear: 0, fromLinear: 0, updated: 0 },
137+
conflicts: [],
138+
errors: [],
139+
};
140+
}
141+
126142
if (!this.config.enabled) {
127143
return {
128144
success: false,

src/utils/__tests__/hook-installer.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ const HOOKS_DIR = path.join(os.homedir(), '.claude', 'hooks');
1818

1919
describe('hook-installer', () => {
2020
describe('CANONICAL_HOOKS', () => {
21-
it('defines all 4 core hooks', () => {
22-
expect(CANONICAL_HOOKS).toHaveLength(4);
21+
it('defines all 5 core hooks', () => {
22+
expect(CANONICAL_HOOKS).toHaveLength(5);
2323
const names = CANONICAL_HOOKS.map((h) => h.scriptName);
2424
expect(names).toContain('session-rescue.sh');
2525
expect(names).toContain('stop-checkpoint.js');
2626
expect(names).toContain('chime-on-stop.sh');
2727
expect(names).toContain('auto-checkpoint.js');
28+
expect(names).toContain('cord-trace.js');
2829
});
2930

3031
it('all core hooks are required', () => {

src/utils/hook-installer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ export const CANONICAL_HOOKS: HookEntry[] = [
5454
commandPrefix: 'node',
5555
required: true,
5656
},
57+
{
58+
scriptName: 'cord-trace.js',
59+
eventType: 'PostToolUse',
60+
matcher: 'mcp__.*__cord_(spawn|fork|complete|ask|tree)',
61+
timeout: 2,
62+
commandPrefix: 'node',
63+
required: true,
64+
},
5765
];
5866

5967
/** Script names that should be removed from settings (dead/deprecated hooks) */

0 commit comments

Comments
 (0)