claude-ws 0.4.9-beta.3 → 0.4.9-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/packages/agentic-sdk/src/lib/output-formatter.ts +16 -2
- package/scripts/db-migrate-health.ts +223 -0
- package/scripts/migrate-existing-projects.ts +107 -0
- package/server.ts +61 -10
- package/src/app/api/agent-factory/projects/[projectId]/sync/route.ts +36 -0
- package/src/hooks/template/hooks/.env.example +3 -0
- package/src/hooks/template/hooks/minio-pull-sync.ts +22 -5
- package/src/hooks/template/hooks/minio-push-sync.ts +91 -0
- package/src/lib/db/index.ts +11 -0
- package/src/lib/output-formatter.ts +4 -0
- package/src/lib/providers/claude-sdk-provider.ts +13 -2
- package/src/lib/providers/claude-sdk-query-options-builder.ts +14 -0
- package/src/types/index.ts +3 -0
- package/tsconfig.json +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-ws",
|
|
3
|
-
"version": "0.4.9-beta.
|
|
3
|
+
"version": "0.4.9-beta.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "AI-powered workspace for solo CEOs and indie builders — manage your entire business with AI agents, not just code. Kanban board, code editor, Git integration, claw agent hub, local-first SQLite.",
|
|
6
6
|
"keywords": [
|
|
@@ -90,9 +90,11 @@
|
|
|
90
90
|
"pm2-logs": "pm2 logs claudews",
|
|
91
91
|
"pm2-monit": "pm2 monit",
|
|
92
92
|
"build": "cross-env NODE_ENV=production next build",
|
|
93
|
-
"start": "cross-env NODE_ENV=production tsx server.ts",
|
|
93
|
+
"start": "pnpm projects:migrate:defaults && cross-env NODE_ENV=production tsx server.ts",
|
|
94
94
|
"db:generate": "drizzle-kit generate",
|
|
95
95
|
"db:fix": "tsx scripts/db-fix-columns.ts",
|
|
96
|
+
"db:migrate:health": "tsx scripts/db-migrate-health.ts",
|
|
97
|
+
"projects:migrate:defaults": "tsx scripts/migrate-existing-projects.ts",
|
|
96
98
|
"migrate:sessions": "tsx scripts/migrate-sdk-sessions.ts"
|
|
97
99
|
},
|
|
98
100
|
"dependencies": {
|
|
@@ -139,9 +141,9 @@
|
|
|
139
141
|
"@tailwindcss/postcss": "^4.2.2",
|
|
140
142
|
"@types/adm-zip": "^0.5.8",
|
|
141
143
|
"@types/better-sqlite3": "^7.6.13",
|
|
142
|
-
"@types/js-yaml": "^4.0.9",
|
|
143
144
|
"@types/compression": "^1.8.1",
|
|
144
145
|
"@types/dompurify": "^3.2.0",
|
|
146
|
+
"@types/js-yaml": "^4.0.9",
|
|
145
147
|
"@types/node": "^20.19.37",
|
|
146
148
|
"@types/react": "^19.2.14",
|
|
147
149
|
"@types/react-dom": "^19.2.3",
|
|
@@ -201,5 +203,8 @@
|
|
|
201
203
|
"optionalDependencies": {
|
|
202
204
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
203
205
|
"node-pty": "^1.1.0"
|
|
206
|
+
},
|
|
207
|
+
"devDependencies": {
|
|
208
|
+
"@types/js-yaml": "^4.0.9"
|
|
204
209
|
}
|
|
205
210
|
}
|
|
@@ -24,6 +24,8 @@ interface ClaudeOutput {
|
|
|
24
24
|
interface FormattedResponse {
|
|
25
25
|
formatted_data: string;
|
|
26
26
|
format: string;
|
|
27
|
+
messages?: ClaudeOutput[];
|
|
28
|
+
status?: string;
|
|
27
29
|
attempt: {
|
|
28
30
|
id: string;
|
|
29
31
|
taskId: string;
|
|
@@ -75,9 +77,21 @@ export function formatOutput(
|
|
|
75
77
|
break;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
formatted_data: formattedData,
|
|
82
|
+
format,
|
|
83
|
+
messages,
|
|
84
|
+
status: attemptMetadata.status as any,
|
|
85
|
+
attempt: { ...attemptMetadata, status: attemptMetadata.status as any }
|
|
86
|
+
};
|
|
79
87
|
} catch {
|
|
80
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
formatted_data: toJson(messages),
|
|
90
|
+
format: 'json',
|
|
91
|
+
messages,
|
|
92
|
+
status: attemptMetadata.status as any,
|
|
93
|
+
attempt: { ...attemptMetadata, status: attemptMetadata.status as any }
|
|
94
|
+
};
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Database health migration script
|
|
4
|
+
*
|
|
5
|
+
* Goals:
|
|
6
|
+
* - Backup database before touching data
|
|
7
|
+
* - Add legacy missing columns (idempotent)
|
|
8
|
+
* - Clean orphan rows that can trigger FK-related runtime errors
|
|
9
|
+
* - Verify FK integrity after cleanup
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* pnpm db:migrate:health
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Database from 'better-sqlite3';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import { config } from 'dotenv';
|
|
19
|
+
|
|
20
|
+
type CleanupItem = {
|
|
21
|
+
label: string;
|
|
22
|
+
run: (db: Database.Database) => number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function nowStamp(): string {
|
|
26
|
+
const d = new Date();
|
|
27
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
28
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function addColumnIfMissing(db: Database.Database, table: string, column: string, type: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureLegacyColumns(db: Database.Database): number {
|
|
41
|
+
let added = 0;
|
|
42
|
+
const attemptsColumns = [
|
|
43
|
+
['total_tokens', 'INTEGER NOT NULL DEFAULT 0'],
|
|
44
|
+
['input_tokens', 'INTEGER NOT NULL DEFAULT 0'],
|
|
45
|
+
['output_tokens', 'INTEGER NOT NULL DEFAULT 0'],
|
|
46
|
+
['cache_creation_tokens', 'INTEGER NOT NULL DEFAULT 0'],
|
|
47
|
+
['cache_read_tokens', 'INTEGER NOT NULL DEFAULT 0'],
|
|
48
|
+
['total_cost_usd', "TEXT NOT NULL DEFAULT '0'"],
|
|
49
|
+
['num_turns', 'INTEGER NOT NULL DEFAULT 0'],
|
|
50
|
+
['duration_ms', 'INTEGER NOT NULL DEFAULT 0'],
|
|
51
|
+
['context_used', 'INTEGER NOT NULL DEFAULT 0'],
|
|
52
|
+
['context_limit', 'INTEGER NOT NULL DEFAULT 200000'],
|
|
53
|
+
['context_percentage', 'INTEGER NOT NULL DEFAULT 0'],
|
|
54
|
+
['baseline_context', 'INTEGER NOT NULL DEFAULT 0'],
|
|
55
|
+
['output_format', 'TEXT'],
|
|
56
|
+
['output_schema', 'TEXT'],
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const tasksColumns = [
|
|
60
|
+
['chat_init', 'INTEGER NOT NULL DEFAULT 0'],
|
|
61
|
+
['rewind_session_id', 'TEXT'],
|
|
62
|
+
['rewind_message_uuid', 'TEXT'],
|
|
63
|
+
['last_model', 'TEXT'],
|
|
64
|
+
['last_provider', 'TEXT'],
|
|
65
|
+
['pending_file_ids', 'TEXT'],
|
|
66
|
+
] as const;
|
|
67
|
+
|
|
68
|
+
const checkpointsColumns = [
|
|
69
|
+
['git_commit_hash', 'TEXT'],
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
for (const [name, type] of attemptsColumns) if (addColumnIfMissing(db, 'attempts', name, type)) added++;
|
|
73
|
+
for (const [name, type] of tasksColumns) if (addColumnIfMissing(db, 'tasks', name, type)) added++;
|
|
74
|
+
for (const [name, type] of checkpointsColumns) if (addColumnIfMissing(db, 'checkpoints', name, type)) added++;
|
|
75
|
+
|
|
76
|
+
return added;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function deleteCount(db: Database.Database, sql: string): number {
|
|
80
|
+
const stmt = db.prepare(sql);
|
|
81
|
+
const result = stmt.run();
|
|
82
|
+
return Number(result.changes || 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function updateCount(db: Database.Database, sql: string): number {
|
|
86
|
+
const stmt = db.prepare(sql);
|
|
87
|
+
const result = stmt.run();
|
|
88
|
+
return Number(result.changes || 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function run(): void {
|
|
92
|
+
const userCwd = process.env.CLAUDE_WS_USER_CWD || process.cwd();
|
|
93
|
+
config({ path: path.join(userCwd, '.env') });
|
|
94
|
+
|
|
95
|
+
const dbDir = process.env.DATA_DIR || path.join(userCwd, 'data');
|
|
96
|
+
const dbPath = path.join(dbDir, 'claude-ws.db');
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(dbPath)) {
|
|
99
|
+
console.log(`[db:migrate:health] DB not found: ${dbPath}`);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
104
|
+
const backupPath = path.join(dbDir, `claude-ws.db.backup-${nowStamp()}`);
|
|
105
|
+
fs.copyFileSync(dbPath, backupPath);
|
|
106
|
+
console.log(`[db:migrate:health] Backup created: ${backupPath}`);
|
|
107
|
+
|
|
108
|
+
const db = new Database(dbPath);
|
|
109
|
+
db.pragma('journal_mode = WAL');
|
|
110
|
+
db.pragma('foreign_keys = ON');
|
|
111
|
+
|
|
112
|
+
const fkBefore = db.prepare('PRAGMA foreign_key_check').all() as any[];
|
|
113
|
+
console.log(`[db:migrate:health] FK violations before: ${fkBefore.length}`);
|
|
114
|
+
|
|
115
|
+
const cleanup: CleanupItem[] = [
|
|
116
|
+
{ label: 'tasks without project', run: (x) => deleteCount(x, `DELETE FROM tasks WHERE project_id NOT IN (SELECT id FROM projects)`) },
|
|
117
|
+
{ label: 'attempts without task', run: (x) => deleteCount(x, `DELETE FROM attempts WHERE task_id NOT IN (SELECT id FROM tasks)`) },
|
|
118
|
+
{ label: 'attempt_logs without attempt', run: (x) => deleteCount(x, `DELETE FROM attempt_logs WHERE attempt_id NOT IN (SELECT id FROM attempts)`) },
|
|
119
|
+
{ label: 'attempt_files without attempt', run: (x) => deleteCount(x, `DELETE FROM attempt_files WHERE attempt_id NOT IN (SELECT id FROM attempts)`) },
|
|
120
|
+
{
|
|
121
|
+
label: 'checkpoints without task/attempt',
|
|
122
|
+
run: (x) => deleteCount(
|
|
123
|
+
x,
|
|
124
|
+
`DELETE FROM checkpoints
|
|
125
|
+
WHERE task_id NOT IN (SELECT id FROM tasks)
|
|
126
|
+
OR attempt_id NOT IN (SELECT id FROM attempts)`
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
label: 'shells with missing attempt -> NULL',
|
|
131
|
+
run: (x) => updateCount(
|
|
132
|
+
x,
|
|
133
|
+
`UPDATE shells
|
|
134
|
+
SET attempt_id = NULL
|
|
135
|
+
WHERE attempt_id IS NOT NULL
|
|
136
|
+
AND attempt_id NOT IN (SELECT id FROM attempts)`
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
label: 'project_plugins orphan refs',
|
|
141
|
+
run: (x) => deleteCount(
|
|
142
|
+
x,
|
|
143
|
+
`DELETE FROM project_plugins
|
|
144
|
+
WHERE project_id NOT IN (SELECT id FROM projects)
|
|
145
|
+
OR plugin_id NOT IN (SELECT id FROM agent_factory_plugins)`
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: 'plugin_dependencies without owner plugin',
|
|
150
|
+
run: (x) => deleteCount(
|
|
151
|
+
x,
|
|
152
|
+
`DELETE FROM plugin_dependencies
|
|
153
|
+
WHERE plugin_id NOT IN (SELECT id FROM agent_factory_plugins)`
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: 'plugin_dependencies missing referenced plugin -> NULL',
|
|
158
|
+
run: (x) => updateCount(
|
|
159
|
+
x,
|
|
160
|
+
`UPDATE plugin_dependencies
|
|
161
|
+
SET plugin_dependency_id = NULL
|
|
162
|
+
WHERE plugin_dependency_id IS NOT NULL
|
|
163
|
+
AND plugin_dependency_id NOT IN (SELECT id FROM agent_factory_plugins)`
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: 'plugin_dependency_cache missing plugin -> NULL',
|
|
168
|
+
run: (x) => updateCount(
|
|
169
|
+
x,
|
|
170
|
+
`UPDATE plugin_dependency_cache
|
|
171
|
+
SET plugin_id = NULL
|
|
172
|
+
WHERE plugin_id IS NOT NULL
|
|
173
|
+
AND plugin_id NOT IN (SELECT id FROM agent_factory_plugins)`
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
label: 'subagents without attempt',
|
|
178
|
+
run: (x) => deleteCount(x, `DELETE FROM subagents WHERE attempt_id NOT IN (SELECT id FROM attempts)`),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: 'tracked_tasks without attempt',
|
|
182
|
+
run: (x) => deleteCount(x, `DELETE FROM tracked_tasks WHERE attempt_id NOT IN (SELECT id FROM attempts)`),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
label: 'agent_messages without attempt',
|
|
186
|
+
run: (x) => deleteCount(x, `DELETE FROM agent_messages WHERE attempt_id NOT IN (SELECT id FROM attempts)`),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const tx = db.transaction(() => {
|
|
191
|
+
const addedColumns = ensureLegacyColumns(db);
|
|
192
|
+
console.log(`[db:migrate:health] Added missing columns: ${addedColumns}`);
|
|
193
|
+
|
|
194
|
+
let totalChanges = 0;
|
|
195
|
+
for (const item of cleanup) {
|
|
196
|
+
const changed = item.run(db);
|
|
197
|
+
totalChanges += changed;
|
|
198
|
+
if (changed > 0) {
|
|
199
|
+
console.log(`[db:migrate:health] ${item.label}: ${changed}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return totalChanges;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const changedRows = tx();
|
|
206
|
+
|
|
207
|
+
const fkAfter = db.prepare('PRAGMA foreign_key_check').all() as any[];
|
|
208
|
+
const quickCheck = db.prepare('PRAGMA quick_check').get() as { quick_check?: string } | undefined;
|
|
209
|
+
db.close();
|
|
210
|
+
|
|
211
|
+
console.log(`[db:migrate:health] Total repaired rows: ${changedRows}`);
|
|
212
|
+
console.log(`[db:migrate:health] FK violations after: ${fkAfter.length}`);
|
|
213
|
+
console.log(`[db:migrate:health] quick_check: ${quickCheck?.quick_check ?? 'unknown'}`);
|
|
214
|
+
|
|
215
|
+
if (fkAfter.length > 0) {
|
|
216
|
+
console.log('[db:migrate:health] WARNING: FK violations remain. Restore backup and inspect manually if needed.');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log('[db:migrate:health] Done.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
run();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Migrate existing projects to current default Claude workspace structure.
|
|
4
|
+
*
|
|
5
|
+
* What it does per project:
|
|
6
|
+
* - Ensures `.claude/hooks` and `.claude/commands` exist
|
|
7
|
+
* - Syncs hook templates (`minio-pull-sync.ts`, `minio-push-sync.ts`)
|
|
8
|
+
* - Ensures `.claude/hooks/.env.example`, `.claude/settings.json`, `.claude/CLAUDE.md`
|
|
9
|
+
* - Creates `.claude/hooks/.env` when missing
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* pnpm projects:migrate:defaults
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { config } from 'dotenv';
|
|
18
|
+
import { db, schema } from '../src/lib/db';
|
|
19
|
+
import { setupProjectDefaults } from '../src/lib/project-utils';
|
|
20
|
+
|
|
21
|
+
type ProjectTarget = {
|
|
22
|
+
id: string;
|
|
23
|
+
projectPath: string;
|
|
24
|
+
source: 'db' | 'scan';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function inferProjectIdFromDirName(dirName: string): string {
|
|
28
|
+
// Folders may be: "<projectId>" or "<projectId>-<name>"
|
|
29
|
+
const firstDash = dirName.indexOf('-');
|
|
30
|
+
if (firstDash <= 0) return dirName;
|
|
31
|
+
return dirName.slice(0, firstDash);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function run(): Promise<void> {
|
|
35
|
+
const workspaceRoot = process.env.CLAUDE_WS_USER_CWD || process.cwd();
|
|
36
|
+
config({ path: path.join(workspaceRoot, '.env') });
|
|
37
|
+
|
|
38
|
+
const dataDir = process.env.DATA_DIR || path.join(workspaceRoot, 'data');
|
|
39
|
+
const projectsDir = path.join(dataDir, 'projects');
|
|
40
|
+
|
|
41
|
+
const targets = new Map<string, ProjectTarget>();
|
|
42
|
+
|
|
43
|
+
// 1) Projects tracked in DB
|
|
44
|
+
const dbProjects = db.select({
|
|
45
|
+
id: schema.projects.id,
|
|
46
|
+
path: schema.projects.path,
|
|
47
|
+
}).from(schema.projects).all();
|
|
48
|
+
|
|
49
|
+
for (const p of dbProjects) {
|
|
50
|
+
targets.set(path.resolve(p.path), {
|
|
51
|
+
id: p.id,
|
|
52
|
+
projectPath: path.resolve(p.path),
|
|
53
|
+
source: 'db',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2) Extra dirs under data/projects not present in DB
|
|
58
|
+
if (fs.existsSync(projectsDir)) {
|
|
59
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory()) continue;
|
|
62
|
+
const projectPath = path.resolve(path.join(projectsDir, entry.name));
|
|
63
|
+
if (targets.has(projectPath)) continue;
|
|
64
|
+
targets.set(projectPath, {
|
|
65
|
+
id: inferProjectIdFromDirName(entry.name),
|
|
66
|
+
projectPath,
|
|
67
|
+
source: 'scan',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (targets.size === 0) {
|
|
73
|
+
console.log('[projects:migrate:defaults] No projects found.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[projects:migrate:defaults] Found ${targets.size} projects.`);
|
|
78
|
+
|
|
79
|
+
let ok = 0;
|
|
80
|
+
let skipped = 0;
|
|
81
|
+
let failed = 0;
|
|
82
|
+
|
|
83
|
+
for (const target of targets.values()) {
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(target.projectPath)) {
|
|
86
|
+
console.log(`[projects:migrate:defaults] SKIP missing path: ${target.projectPath}`);
|
|
87
|
+
skipped++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await setupProjectDefaults(target.projectPath, target.id, workspaceRoot);
|
|
92
|
+
console.log(`[projects:migrate:defaults] OK ${target.source} id=${target.id} path=${target.projectPath}`);
|
|
93
|
+
ok++;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[projects:migrate:defaults] FAIL id=${target.id} path=${target.projectPath}`, error);
|
|
96
|
+
failed++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`[projects:migrate:defaults] Done. ok=${ok} skipped=${skipped} failed=${failed}`);
|
|
101
|
+
if (failed > 0) process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
run().catch((error) => {
|
|
105
|
+
console.error('[projects:migrate:defaults] Fatal error', error);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
package/server.ts
CHANGED
|
@@ -59,6 +59,13 @@ import type { AutopilotMode } from './src/lib/autopilot';
|
|
|
59
59
|
|
|
60
60
|
import { getPort, getHostname } from './src/lib/server-port-configuration';
|
|
61
61
|
|
|
62
|
+
function isSqliteForeignKeyError(error: unknown): boolean {
|
|
63
|
+
if (!error || typeof error !== 'object') return false;
|
|
64
|
+
const maybeError = error as { code?: string; message?: string };
|
|
65
|
+
return maybeError.code === 'SQLITE_CONSTRAINT_FOREIGNKEY'
|
|
66
|
+
|| (typeof maybeError.message === 'string' && maybeError.message.includes('FOREIGN KEY constraint failed'));
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
const dev = process.env.NODE_ENV !== 'production';
|
|
63
70
|
const hostname = getHostname();
|
|
64
71
|
const port = getPort();
|
|
@@ -72,6 +79,26 @@ app.prepare().then(async () => {
|
|
|
72
79
|
const httpServer = createServer((req, res) => {
|
|
73
80
|
const parsedUrl = parse(req.url!, true);
|
|
74
81
|
const pathname = parsedUrl.pathname || '';
|
|
82
|
+
const requestStartedAt = Date.now();
|
|
83
|
+
const shouldTraceRequest =
|
|
84
|
+
pathname.startsWith('/api/attempts')
|
|
85
|
+
|| pathname.includes('/conversation')
|
|
86
|
+
|| pathname.includes('/running-attempt')
|
|
87
|
+
|| pathname.includes('/pending-question')
|
|
88
|
+
|| pathname.includes('/agent-factory/projects/');
|
|
89
|
+
|
|
90
|
+
if (shouldTraceRequest) {
|
|
91
|
+
res.on('finish', () => {
|
|
92
|
+
const durationMs = Date.now() - requestStartedAt;
|
|
93
|
+
log.info({
|
|
94
|
+
method: req.method,
|
|
95
|
+
path: pathname,
|
|
96
|
+
statusCode: res.statusCode,
|
|
97
|
+
durationMs,
|
|
98
|
+
hasApiKey: Boolean(req.headers['x-api-key']),
|
|
99
|
+
}, '[Server] HTTP request traced');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
75
102
|
|
|
76
103
|
// API authentication check - read from process.env directly for immediate effect
|
|
77
104
|
const apiAccessKey = process.env.API_ACCESS_KEY;
|
|
@@ -1036,11 +1063,19 @@ app.prepare().then(async () => {
|
|
|
1036
1063
|
|
|
1037
1064
|
if (!isStreamingDelta) {
|
|
1038
1065
|
// Save to database (only complete messages)
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1066
|
+
try {
|
|
1067
|
+
await db.insert(schema.attemptLogs).values({
|
|
1068
|
+
attemptId,
|
|
1069
|
+
type: 'json',
|
|
1070
|
+
content: JSON.stringify(data),
|
|
1071
|
+
});
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
if (isSqliteForeignKeyError(error)) {
|
|
1074
|
+
log.warn({ attemptId }, '[Server] Skipping JSON log write because attempt no longer exists');
|
|
1075
|
+
} else {
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1044
1079
|
}
|
|
1045
1080
|
|
|
1046
1081
|
// Check how many clients are in the room
|
|
@@ -1106,11 +1141,19 @@ app.prepare().then(async () => {
|
|
|
1106
1141
|
});
|
|
1107
1142
|
|
|
1108
1143
|
agentManager.on('stderr', async ({ attemptId, content }) => {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1144
|
+
try {
|
|
1145
|
+
await db.insert(schema.attemptLogs).values({
|
|
1146
|
+
attemptId,
|
|
1147
|
+
type: 'stderr',
|
|
1148
|
+
content,
|
|
1149
|
+
});
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
if (isSqliteForeignKeyError(error)) {
|
|
1152
|
+
log.warn({ attemptId }, '[Server] Skipping stderr log write because attempt no longer exists');
|
|
1153
|
+
} else {
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1114
1157
|
|
|
1115
1158
|
io.to(`attempt:${attemptId}`).emit('output:stderr', { attemptId, content });
|
|
1116
1159
|
});
|
|
@@ -1382,6 +1425,7 @@ app.prepare().then(async () => {
|
|
|
1382
1425
|
|
|
1383
1426
|
// Register exit event handler
|
|
1384
1427
|
agentManager.on('exit', async ({ attemptId, code }) => {
|
|
1428
|
+
try {
|
|
1385
1429
|
// Get attempt to retrieve taskId and current status
|
|
1386
1430
|
const attempt = await db.query.attempts.findFirst({
|
|
1387
1431
|
where: eq(schema.attempts.id, attemptId),
|
|
@@ -1599,6 +1643,13 @@ app.prepare().then(async () => {
|
|
|
1599
1643
|
usageTracker.clearSession(attemptId);
|
|
1600
1644
|
workflowTracker.clearWorkflow(attemptId);
|
|
1601
1645
|
gitStatsCache.clear(attemptId);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
if (isSqliteForeignKeyError(error)) {
|
|
1648
|
+
log.warn({ attemptId }, '[Server] Ignoring FK error in exit handler (attempt/task deleted)');
|
|
1649
|
+
} else {
|
|
1650
|
+
log.error({ attemptId, error }, '[Server] Exit handler failed');
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1602
1653
|
});
|
|
1603
1654
|
|
|
1604
1655
|
// Forward tracking module events to Socket.io clients
|
|
@@ -82,6 +82,14 @@ export async function POST(
|
|
|
82
82
|
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
log.info(
|
|
86
|
+
{
|
|
87
|
+
projectId,
|
|
88
|
+
projectPath: project.path,
|
|
89
|
+
},
|
|
90
|
+
'Resolved project path for sync'
|
|
91
|
+
);
|
|
92
|
+
|
|
85
93
|
let parsedBody: unknown = {};
|
|
86
94
|
try {
|
|
87
95
|
parsedBody = await request.json();
|
|
@@ -95,6 +103,7 @@ export async function POST(
|
|
|
95
103
|
log.info(
|
|
96
104
|
{
|
|
97
105
|
projectId,
|
|
106
|
+
projectPath: project.path,
|
|
98
107
|
body,
|
|
99
108
|
componentIds,
|
|
100
109
|
agentSetIds,
|
|
@@ -106,12 +115,39 @@ export async function POST(
|
|
|
106
115
|
await upsertProjectSettings(project.path, componentIds, agentSetIds);
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
log.info(
|
|
119
|
+
{
|
|
120
|
+
projectId,
|
|
121
|
+
projectPath: project.path,
|
|
122
|
+
},
|
|
123
|
+
'Starting AgentFactory project sync'
|
|
124
|
+
);
|
|
125
|
+
|
|
109
126
|
const result = await agentFactoryService.syncProject(projectId, project.path);
|
|
110
127
|
|
|
111
128
|
if (!result.success && result.error) {
|
|
129
|
+
log.warn(
|
|
130
|
+
{
|
|
131
|
+
projectId,
|
|
132
|
+
projectPath: project.path,
|
|
133
|
+
error: result.error,
|
|
134
|
+
},
|
|
135
|
+
'AgentFactory project sync failed'
|
|
136
|
+
);
|
|
112
137
|
return NextResponse.json({ error: result.error }, { status: 400 });
|
|
113
138
|
}
|
|
114
139
|
|
|
140
|
+
log.info(
|
|
141
|
+
{
|
|
142
|
+
projectId,
|
|
143
|
+
projectPath: project.path,
|
|
144
|
+
installedCount: result.installed?.length ?? 0,
|
|
145
|
+
skippedCount: result.skipped?.length ?? 0,
|
|
146
|
+
errorCount: result.errors?.length ?? 0,
|
|
147
|
+
},
|
|
148
|
+
'AgentFactory project sync completed'
|
|
149
|
+
);
|
|
150
|
+
|
|
115
151
|
return NextResponse.json({
|
|
116
152
|
success: true,
|
|
117
153
|
message: `Installed ${result.installed?.length ?? 0} components to project`,
|
|
@@ -7,4 +7,7 @@
|
|
|
7
7
|
API_HOOK_URL_LOCAL="http://localhost:5005/api/sync/"
|
|
8
8
|
API_HOOK_URL_DOMAIN="https://privos-chat-dev.roxane.one/api/v1/internal/rooms/room_id/files/"
|
|
9
9
|
API_HOOK_API_KEY=
|
|
10
|
+
API_QUEUE_URL="http://localhost:8052"
|
|
11
|
+
# Optional comma-separated extra queue endpoints/base URLs
|
|
12
|
+
# API_QUEUE_FALLBACK_URLS="http://localhost:8053,http://localhost:3000"
|
|
10
13
|
PROJECT_ID="your-project-id"
|
|
@@ -20,6 +20,8 @@ if (!config.apiBaseUrl) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const PULL_DB_PATH = path.join(hooksDir, "pull-sync-state.db");
|
|
23
|
+
const TMP_DIR = path.join(process.cwd(), ".claude", "tmp");
|
|
24
|
+
const PUSH_STATE_FILE = path.join(TMP_DIR, "local-sync-state.json");
|
|
23
25
|
|
|
24
26
|
function buildApiUrl(endpoint: string): string {
|
|
25
27
|
const base = String(config.apiBaseUrl || "").replace(/\/+$/g, "");
|
|
@@ -111,12 +113,17 @@ async function ensurePullDb(): Promise<Database.Database> {
|
|
|
111
113
|
return sqlite;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
async function fetchManifest(folder: string, label: string): Promise<ManifestEntry[]> {
|
|
116
|
+
async function fetchManifest(folder: string, label: string, allowNotFound = false): Promise<ManifestEntry[]> {
|
|
115
117
|
console.error(`🔍 Calling API to get manifest for '${label}' (${folder})...`);
|
|
116
118
|
|
|
117
119
|
const url = buildApiUrl(`manifest?folder=${encodeURIComponent(folder)}`);
|
|
118
120
|
const response = await fetch(url, { headers: buildApiHeaders() });
|
|
119
121
|
|
|
122
|
+
if (response.status === 404 && allowNotFound) {
|
|
123
|
+
console.error(`ℹ️ ${label} not found on remote, treating as empty manifest.`);
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
if (!response.ok) {
|
|
121
128
|
throw new Error(`API manifest failed for ${label}: HTTP ${response.status} ${response.statusText}`);
|
|
122
129
|
}
|
|
@@ -132,13 +139,13 @@ async function fetchManifest(folder: string, label: string): Promise<ManifestEnt
|
|
|
132
139
|
return objects;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
async function getQueueCandidates(): Promise<QueueFileCandidate[]> {
|
|
142
|
+
async function getQueueCandidates(): Promise<{ candidates: QueueFileCandidate[]; manifestData: ManifestEntry[] }> {
|
|
136
143
|
const mainPrefix = config.projectId;
|
|
137
144
|
const markdownPrefix = `markdown/${config.projectId}`;
|
|
138
145
|
|
|
139
146
|
const [mainManifest, markdownManifest] = await Promise.all([
|
|
140
147
|
fetchManifest(mainPrefix, "main folder"),
|
|
141
|
-
fetchManifest(markdownPrefix, "markdown folder"),
|
|
148
|
+
fetchManifest(markdownPrefix, "markdown folder", true),
|
|
142
149
|
]);
|
|
143
150
|
|
|
144
151
|
const normalize = (entries: ManifestEntry[], folder: FolderType): QueueFileCandidate[] => (
|
|
@@ -152,7 +159,15 @@ async function getQueueCandidates(): Promise<QueueFileCandidate[]> {
|
|
|
152
159
|
}))
|
|
153
160
|
);
|
|
154
161
|
|
|
155
|
-
return
|
|
162
|
+
return {
|
|
163
|
+
candidates: [...normalize(mainManifest, "main"), ...normalize(markdownManifest, "markdown")],
|
|
164
|
+
manifestData: [...mainManifest, ...markdownManifest],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function writePushStateFile(manifestData: ManifestEntry[]): Promise<void> {
|
|
169
|
+
await fs.mkdir(TMP_DIR, { recursive: true });
|
|
170
|
+
await fs.writeFile(PUSH_STATE_FILE, JSON.stringify(manifestData, null, 2), "utf-8");
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
function enqueueCandidates(
|
|
@@ -249,12 +264,14 @@ async function runEnqueue() {
|
|
|
249
264
|
let sqlite: Database.Database | null = null;
|
|
250
265
|
try {
|
|
251
266
|
console.error("\n=================== ENQUEUE MINIO PULL JOB ===================");
|
|
252
|
-
const candidates = await getQueueCandidates();
|
|
267
|
+
const { candidates, manifestData } = await getQueueCandidates();
|
|
268
|
+
await writePushStateFile(manifestData);
|
|
253
269
|
|
|
254
270
|
sqlite = await ensurePullDb();
|
|
255
271
|
const result = enqueueCandidates(sqlite, candidates);
|
|
256
272
|
|
|
257
273
|
console.error(`✅ Queue DB: ${PULL_DB_PATH}`);
|
|
274
|
+
console.error(`✅ Push state file: ${PUSH_STATE_FILE}`);
|
|
258
275
|
console.error(`🧾 Job ID: ${result.jobId}`);
|
|
259
276
|
console.error(`📊 Total: ${result.totalFiles} | Enqueued: ${result.enqueuedFiles} | Skipped: ${result.skippedFiles}`);
|
|
260
277
|
console.error(`📌 Job status: ${result.status}`);
|
|
@@ -35,6 +35,12 @@ function createConcurrencyLimit(concurrency: number) {
|
|
|
35
35
|
const config = {
|
|
36
36
|
apiBaseUrl: process.env.API_HOOK_URL as string,
|
|
37
37
|
apiHookApiKey: (process.env.API_HOOK_API_KEY || "").trim(),
|
|
38
|
+
apiQueueUrl: (process.env.API_QUEUE_URL || process.env.CLAUDE_WS_API_BASE_URL || "").trim(),
|
|
39
|
+
apiQueueFallbackUrls: (process.env.API_QUEUE_FALLBACK_URLS || "").trim(),
|
|
40
|
+
apiQueueKey: (process.env.API_HOOK_KEY || process.env.API_ACCESS_KEY || "").trim(),
|
|
41
|
+
appPort: (process.env.PORT || "").trim(),
|
|
42
|
+
queueHost: (process.env.API_QUEUE_HOST || "localhost").trim(),
|
|
43
|
+
projectId: (process.env.PROJECT_ID || "__PROJECT_ID__") as string,
|
|
38
44
|
targetPrefix: (process.env.PROJECT_ID || "__PROJECT_ID__") as string,
|
|
39
45
|
};
|
|
40
46
|
|
|
@@ -63,6 +69,83 @@ function buildApiHeaders(baseHeaders: Record<string, string> = {}): HeadersInit
|
|
|
63
69
|
return headers;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
function toPushEndpoint(baseOrEndpoint: string): string {
|
|
73
|
+
const trimmed = String(baseOrEndpoint || "").trim().replace(/\/+$/g, "");
|
|
74
|
+
if (!trimmed) return "";
|
|
75
|
+
if (/\/api\/sync\/minio\/push$/i.test(trimmed)) return trimmed;
|
|
76
|
+
return `${trimmed}/api/sync/minio/push`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildQueueCandidates(): string[] {
|
|
80
|
+
const candidates: string[] = [];
|
|
81
|
+
const push = (value: string) => {
|
|
82
|
+
const endpoint = toPushEndpoint(value);
|
|
83
|
+
if (endpoint && !candidates.includes(endpoint)) {
|
|
84
|
+
candidates.push(endpoint);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Highest priority: explicit queue base URL
|
|
89
|
+
if (config.apiQueueUrl) {
|
|
90
|
+
push(config.apiQueueUrl);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Default queue endpoint: localhost + PORT from environment
|
|
94
|
+
if (!config.apiQueueUrl && config.appPort) {
|
|
95
|
+
push(`http://${config.queueHost}:${config.appPort}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Optional comma-separated list of extra queue base URLs/endpoints
|
|
99
|
+
if (config.apiQueueFallbackUrls) {
|
|
100
|
+
for (const raw of config.apiQueueFallbackUrls.split(",")) {
|
|
101
|
+
const value = raw.trim();
|
|
102
|
+
if (value) push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Last fallback: same origin as sync API base
|
|
107
|
+
try {
|
|
108
|
+
const origin = new URL(config.apiBaseUrl).origin;
|
|
109
|
+
push(origin);
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore invalid URL
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return candidates;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function enqueuePushJob(): Promise<boolean> {
|
|
118
|
+
const endpoints = buildQueueCandidates();
|
|
119
|
+
if (endpoints.length === 0) return false;
|
|
120
|
+
|
|
121
|
+
for (const endpoint of endpoints) {
|
|
122
|
+
try {
|
|
123
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
124
|
+
if (config.apiQueueKey) {
|
|
125
|
+
headers["x-api-key"] = config.apiQueueKey;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const response = await fetch(endpoint, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers,
|
|
131
|
+
body: JSON.stringify({ projectId: config.projectId }),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (response.ok) {
|
|
135
|
+
console.error(`✅ Queue accepted via ${endpoint}`);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.error(`⚠️ Queue endpoint failed ${endpoint}: HTTP ${response.status}`);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
console.error(`⚠️ Queue endpoint unreachable ${endpoint}: ${message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
66
149
|
// Function to create tmp directory
|
|
67
150
|
async function ensureTmpDir() {
|
|
68
151
|
await fs.mkdir(TMP_DIR, { recursive: true });
|
|
@@ -138,6 +221,14 @@ async function scanDirectory(dir: string, fileList: string[] = []) {
|
|
|
138
221
|
// MAIN SYNC FUNCTION
|
|
139
222
|
// ==========================================
|
|
140
223
|
async function runCheck() {
|
|
224
|
+
// Preferred path: enqueue into server-side push queue (records jobs in push-sync-state.db)
|
|
225
|
+
const enqueued = await enqueuePushJob();
|
|
226
|
+
if (enqueued) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.error("⚠️ Queue enqueue failed on all endpoints, falling back to direct upload mode.");
|
|
231
|
+
|
|
141
232
|
await ensureTmpDir(); // Create tmp directory before running
|
|
142
233
|
console.error(`🔍 Starting to scan files in '${LOCAL_DATA_DIR}' and compare with '${STATE_FILE}'...`);
|
|
143
234
|
|
package/src/lib/db/index.ts
CHANGED
|
@@ -268,6 +268,17 @@ export function initDb() {
|
|
|
268
268
|
// Column already exists
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
// Cleanup inconsistent legacy rows created while FK checks were disabled.
|
|
273
|
+
// Keeps runtime stable and avoids unexpected FK errors in later writes.
|
|
274
|
+
sqlite.exec(`
|
|
275
|
+
DELETE FROM checkpoints
|
|
276
|
+
WHERE task_id NOT IN (SELECT id FROM tasks)
|
|
277
|
+
OR attempt_id NOT IN (SELECT id FROM attempts);
|
|
278
|
+
|
|
279
|
+
DELETE FROM attempts
|
|
280
|
+
WHERE task_id NOT IN (SELECT id FROM tasks);
|
|
281
|
+
`);
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
// Initialize on first import
|
|
@@ -52,6 +52,8 @@ export function formatOutput(
|
|
|
52
52
|
return {
|
|
53
53
|
formatted_data: formattedData,
|
|
54
54
|
format,
|
|
55
|
+
messages,
|
|
56
|
+
status: attemptMetadata.status as any,
|
|
55
57
|
attempt: {
|
|
56
58
|
id: attemptMetadata.id,
|
|
57
59
|
taskId: attemptMetadata.taskId,
|
|
@@ -66,6 +68,8 @@ export function formatOutput(
|
|
|
66
68
|
return {
|
|
67
69
|
formatted_data: toJson(messages),
|
|
68
70
|
format: 'json',
|
|
71
|
+
messages,
|
|
72
|
+
status: attemptMetadata.status as any,
|
|
69
73
|
attempt: {
|
|
70
74
|
id: attemptMetadata.id,
|
|
71
75
|
taskId: attemptMetadata.taskId,
|
|
@@ -101,12 +101,23 @@ export class ClaudeSDKProvider extends EventEmitter implements Provider {
|
|
|
101
101
|
canUseToolCallback: this.makeCanUseTool(attemptId),
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
const promptPreview = prompt.substring(0, 400) + (prompt.length > 400 ? '...' : '');
|
|
105
|
+
const shouldLogFullPrompt = process.env.SDK_LOG_FULL_PROMPT === '1';
|
|
106
|
+
|
|
104
107
|
log.info({
|
|
108
|
+
attemptId,
|
|
105
109
|
endpoint: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
projectPath,
|
|
111
|
+
cwd: opts.cwd,
|
|
112
|
+
model: opts.model,
|
|
113
|
+
promptLength: prompt.length,
|
|
114
|
+
promptPreview,
|
|
108
115
|
}, 'SDK Query starting');
|
|
109
116
|
|
|
117
|
+
if (shouldLogFullPrompt) {
|
|
118
|
+
log.info({ attemptId, projectPath, prompt }, 'SDK Query full prompt');
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
const response = query({ prompt, options: opts });
|
|
111
122
|
session.queryRef = response;
|
|
112
123
|
|
|
@@ -41,6 +41,19 @@ export interface QueryOptionsBuilderParams {
|
|
|
41
41
|
canUseToolCallback: CanUseToolCallback;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
type SettingSource = 'user' | 'project' | 'local';
|
|
45
|
+
|
|
46
|
+
function resolveSettingSources(): SettingSource[] {
|
|
47
|
+
const allowed = new Set<SettingSource>(['user', 'project', 'local']);
|
|
48
|
+
const raw = process.env.CLAUDE_SDK_SETTING_SOURCES;
|
|
49
|
+
if (!raw || raw.trim().length === 0) return ['project'];
|
|
50
|
+
const values = raw
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((value) => value.trim())
|
|
53
|
+
.filter((value): value is SettingSource => allowed.has(value as SettingSource));
|
|
54
|
+
return values.length > 0 ? values : ['project'];
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
/**
|
|
45
58
|
* Build the options object for SDK query(), minus prompt.
|
|
46
59
|
*/
|
|
@@ -74,6 +87,7 @@ export function buildQueryOptions(params: QueryOptionsBuilderParams) {
|
|
|
74
87
|
...(maxTurns ? { maxTurns } : {}),
|
|
75
88
|
abortController: controller,
|
|
76
89
|
canUseTool: canUseToolCallback,
|
|
90
|
+
settingSources: resolveSettingSources(),
|
|
77
91
|
env: buildIsolatedSubprocessEnv(model),
|
|
78
92
|
};
|
|
79
93
|
|
package/src/types/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type RequestMethod = 'sync' | 'queue';
|
|
|
14
14
|
export interface FormattedResponse {
|
|
15
15
|
formatted_data: string;
|
|
16
16
|
format: OutputFormat;
|
|
17
|
+
// Backward-compatible fields for clients expecting parsed messages/status.
|
|
18
|
+
messages?: ClaudeOutput[];
|
|
19
|
+
status?: AttemptStatus;
|
|
17
20
|
attempt: {
|
|
18
21
|
id: string;
|
|
19
22
|
taskId: string;
|