shraga 0.1.56 → 0.1.57
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { signInternalToken } from '../auth.ts';
|
|
@@ -118,18 +119,44 @@ function logCacheUsage(usage: any, model: string): void {
|
|
|
118
119
|
console.log(`[claude] Cache: hit=${hitRate}% read=${read} write=${created} uncached=${fresh} out=${usage.output_tokens ?? 0} model=${model}`);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/** SDK spawn hook: same call the SDK would make, plus `detached` (own process group). See the
|
|
123
|
+
* call site for why. Shape mirrors the SDK's own spawnLocalProcess return. */
|
|
124
|
+
function spawnDetached(cfg: { command: string; args: string[]; cwd?: string; env: Record<string, string | undefined>; signal?: AbortSignal }) {
|
|
125
|
+
const child = spawn(cfg.command, cfg.args, {
|
|
126
|
+
cwd: cfg.cwd,
|
|
127
|
+
env: cfg.env,
|
|
128
|
+
signal: cfg.signal,
|
|
129
|
+
// Keep SDK debug output visible when it is asked for; otherwise the SDK's own default.
|
|
130
|
+
stdio: ['pipe', 'pipe', cfg.env.DEBUG_CLAUDE_AGENT_SDK ? 'inherit' : 'ignore'],
|
|
131
|
+
detached: true,
|
|
132
|
+
windowsHide: true,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
stdin: child.stdin,
|
|
136
|
+
stdout: child.stdout,
|
|
137
|
+
get killed() { return child.killed; },
|
|
138
|
+
get exitCode() { return child.exitCode; },
|
|
139
|
+
kill: child.kill.bind(child),
|
|
140
|
+
on: child.on.bind(child),
|
|
141
|
+
once: child.once.bind(child),
|
|
142
|
+
off: child.off.bind(child),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
const INLINE_MIMES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']);
|
|
122
147
|
|
|
123
148
|
async function* buildAttachmentPrompt(text: string, attachments: { path: string; name: string; mimeType: string }[], sessionId: string): AsyncIterable<any> {
|
|
124
149
|
const content: any[] = [];
|
|
125
150
|
const fileRefs: string[] = [];
|
|
126
151
|
const audioRefs: string[] = [];
|
|
152
|
+
const inlined: string[] = [];
|
|
127
153
|
for (const att of attachments) {
|
|
128
154
|
if (INLINE_MIMES.has(att.mimeType)) {
|
|
129
155
|
try {
|
|
130
156
|
const buf = readFileSync(att.path);
|
|
131
157
|
const blockType = att.mimeType === 'application/pdf' ? 'document' : 'image';
|
|
132
158
|
content.push({ type: blockType, source: { type: 'base64', media_type: att.mimeType, data: buf.toString('base64') } });
|
|
159
|
+
inlined.push(`${att.name} (${att.path})`);
|
|
133
160
|
} catch (err) {
|
|
134
161
|
console.error(`[claude] Failed to read attachment ${att.path}:`, err);
|
|
135
162
|
fileRefs.push(`${att.name} (at ${att.path} — failed to read)`);
|
|
@@ -143,6 +170,12 @@ async function* buildAttachmentPrompt(text: string, attachments: { path: string;
|
|
|
143
170
|
}
|
|
144
171
|
if (audioRefs.length > 0) text += `\n\n[Audio attached — transcribe with the mcp-audio tool (post_audio_transcribe { file }) before answering]: ${audioRefs.join(', ')}`;
|
|
145
172
|
if (fileRefs.length > 0) text += `\n\n[Attached files — use Read tool to access]: ${fileRefs.join(', ')}`;
|
|
173
|
+
// Say IN THE TEXT that the inlined blocks exist. `text` carries the whole conversation history, so
|
|
174
|
+
// the image blocks sit tens of thousands of tokens above the actual question and the model has
|
|
175
|
+
// answered "I don't see any images attached" to a message that had four — the transcript proves the
|
|
176
|
+
// blocks were sent. This line makes the prompt agree with its own content, and names the on-disk
|
|
177
|
+
// paths so the model can Read them if it still can't see a block.
|
|
178
|
+
if (inlined.length > 0) text += `\n\n[${inlined.length} file(s) are attached to THIS message and included above as image/document blocks — look at them: ${inlined.join(', ')}]`;
|
|
146
179
|
content.push({ type: 'text', text });
|
|
147
180
|
yield { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null, session_id: sessionId };
|
|
148
181
|
}
|
|
@@ -273,6 +306,14 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
273
306
|
const addonSuffix = getPromptSuffix(opts.turnHints);
|
|
274
307
|
options['systemPrompt'] = `${IMMUTABLE_SYSTEM_PROMPT}\n\n${userPrompt}${addonSuffix ? `\n\n${addonSuffix}` : ''}`;
|
|
275
308
|
if (opts.abortController) options['abortController'] = opts.abortController;
|
|
309
|
+
// Spawn the CLI in its OWN process group. The service manager signals the whole JOB on restart
|
|
310
|
+
// (`launchctl kickstart -k`, `systemctl restart`), so an inherited process group means the child
|
|
311
|
+
// dies instantly with SIGTERM — surfacing mid-reply as `exited with code 143` and making the
|
|
312
|
+
// server's 90s drain (gracefulShutdown) protect nothing: it only ever waited for a turn that was
|
|
313
|
+
// already dead. Detached, the signal reaches the server alone and the drain can finish the turn.
|
|
314
|
+
// Teardown is unaffected: the SDK still kills the child on abort/close and on process exit.
|
|
315
|
+
options['spawnClaudeCodeProcess'] = spawnDetached;
|
|
316
|
+
|
|
276
317
|
// Passed as a FILE, never as `options.mcpServers` — the SDK would put the whole config (every MCP
|
|
277
318
|
// server's credentials) on the CLI's argv, where `ps` / `/proc` / journald expose it. See
|
|
278
319
|
// writeMcpConfigFile. Setting both would re-add the argv copy, so it's one or the other.
|
|
@@ -72,6 +72,13 @@ export class SelfUpgrade {
|
|
|
72
72
|
if (running.length) {
|
|
73
73
|
reasons.push(`${running.length} scheduled job(s) still running (${running.join(', ')}) — restarting now would kill them mid-flight; retry when idle or pass {"force":true}`);
|
|
74
74
|
}
|
|
75
|
+
// Same failure, other origin: an interactive Slack/web turn is not a scheduled job, so the
|
|
76
|
+
// check above never saw it and upgrades cut live replies off mid-sentence (143). Any held
|
|
77
|
+
// session lock means someone is mid-turn.
|
|
78
|
+
const turns = this.o.runningTurns();
|
|
79
|
+
if (turns) {
|
|
80
|
+
reasons.push(`${turns} agent turn(s) in flight — restarting now would cut the reply off mid-stream; retry when idle or pass {"force":true}`);
|
|
81
|
+
}
|
|
75
82
|
}
|
|
76
83
|
const pkgPath = path.join(this.o.appRoot, 'package.json');
|
|
77
84
|
|
|
@@ -254,6 +261,13 @@ export class SelfUpgradeOptions {
|
|
|
254
261
|
catch (err) { console.warn(`${TAG} could not read running jobs:`, (err as Error).message); return []; }
|
|
255
262
|
};
|
|
256
263
|
|
|
264
|
+
/** Interactive agent turns in flight (Slack/web/API), counted off the live session locks. Same
|
|
265
|
+
* lazy+guarded shape as runningJobs, and injectable for the same reason. */
|
|
266
|
+
public runningTurns: () => number = () => {
|
|
267
|
+
try { return require('../sessions.ts').getActiveLockCount() as number; }
|
|
268
|
+
catch (err) { console.warn(`${TAG} could not read running turns:`, (err as Error).message); return 0; }
|
|
269
|
+
};
|
|
270
|
+
|
|
257
271
|
/** Injectable so the preflight is testable without depending on the test host's PATH. */
|
|
258
272
|
public hasPython3: () => boolean = () => {
|
|
259
273
|
try { return spawnSync('python3', ['--version'], { stdio: 'ignore' }).status === 0; }
|