shraga 0.1.42 → 0.1.44
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 +2 -2
- package/src/server/data-sync.ts +13 -21
- package/src/server/directives.ts +34 -4
- package/src/server/engine/claude-code.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
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",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"express": "^4.21.1",
|
|
71
71
|
"firebase": "^11.0.0",
|
|
72
72
|
"lucide-react": "^0.468.0",
|
|
73
|
-
"mcp-slack-use": "github:Livshitz/mcp-slack-use#
|
|
73
|
+
"mcp-slack-use": "github:Livshitz/mcp-slack-use#38ff8b7",
|
|
74
74
|
"react": "^19.0.0",
|
|
75
75
|
"react-dom": "^19.0.0",
|
|
76
76
|
"react-markdown": "^9.0.0",
|
package/src/server/data-sync.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'no
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DATA_DIR } from './paths.ts';
|
|
4
4
|
import { emitEvent } from './events/bus.ts';
|
|
5
|
+
import { runTextQuery } from './sdk-utils.ts';
|
|
5
6
|
|
|
6
7
|
const TAG = '[data-sync]';
|
|
7
8
|
const DEPLOYMENT_ID_FILE = '.deployment-id';
|
|
@@ -340,7 +341,7 @@ export class DataSync {
|
|
|
340
341
|
const msg = await this.askClaude(
|
|
341
342
|
'Write a concise git commit message (max 72 chars, no quotes, no prefix like "feat:" or "sync:") for this change to an AI agent\'s behavioral config.\n' +
|
|
342
343
|
`Files: ${files.join(', ')}\nStats: ${diff}\n\nDiff:\n${truncated}`,
|
|
343
|
-
'
|
|
344
|
+
'haiku',
|
|
344
345
|
);
|
|
345
346
|
const line = msg.split('\n')[0].trim().slice(0, 72);
|
|
346
347
|
return line || fallback;
|
|
@@ -382,7 +383,7 @@ export class DataSync {
|
|
|
382
383
|
'Output format:\n' +
|
|
383
384
|
'<file path="<path>" complexity="trivial|ambiguous" reason="short explanation">\n<resolved content>\n</file>\n' +
|
|
384
385
|
'Output ONLY the resolved files in this format.\n\n' + sections,
|
|
385
|
-
'
|
|
386
|
+
'opus',
|
|
386
387
|
);
|
|
387
388
|
|
|
388
389
|
const parsed = this.parseResolvedFiles(resolved);
|
|
@@ -462,7 +463,7 @@ export class DataSync {
|
|
|
462
463
|
'Output format:\n' +
|
|
463
464
|
'<file path="<path>" complexity="trivial|ambiguous" reason="short explanation">\n<resolved content>\n</file>\n' +
|
|
464
465
|
'Output ONLY the resolved files.\n\n' + sections,
|
|
465
|
-
'
|
|
466
|
+
'opus',
|
|
466
467
|
);
|
|
467
468
|
|
|
468
469
|
const parsed = this.parseResolvedFiles(resolved);
|
|
@@ -564,24 +565,15 @@ export class DataSync {
|
|
|
564
565
|
emitEvent('data-sync', { kind: 'deploy', owners, text });
|
|
565
566
|
}
|
|
566
567
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
},
|
|
577
|
-
body: JSON.stringify({
|
|
578
|
-
model, max_tokens: maxTokens,
|
|
579
|
-
messages: [{ role: 'user', content: prompt }],
|
|
580
|
-
}),
|
|
581
|
-
});
|
|
582
|
-
if (!resp.ok) throw new Error(`Anthropic API ${resp.status}: ${await resp.text()}`);
|
|
583
|
-
const data = await resp.json() as { content: { type: string; text: string }[] };
|
|
584
|
-
return data.content.find(b => b.type === 'text')?.text || '';
|
|
568
|
+
/**
|
|
569
|
+
* Routed through the Claude Code SDK (runTextQuery), same as the rest of the platform —
|
|
570
|
+
* authenticates via the CC subscription, no ANTHROPIC_API_KEY required. Past incident:
|
|
571
|
+
* this used to hit the raw Anthropic Messages API directly with process.env.ANTHROPIC_API_KEY,
|
|
572
|
+
* which is unset on subscription-auth deployments — every merge-conflict resolution failed and
|
|
573
|
+
* spammed owners via notifyOwners().
|
|
574
|
+
*/
|
|
575
|
+
private async askClaude(prompt: string, model: 'haiku' | 'sonnet' | 'opus' = 'sonnet'): Promise<string> {
|
|
576
|
+
return runTextQuery({ prompt, model, maxTurns: 1 });
|
|
585
577
|
}
|
|
586
578
|
|
|
587
579
|
private async getConflictedFiles(): Promise<string[]> {
|
package/src/server/directives.ts
CHANGED
|
@@ -23,12 +23,42 @@ import { MODEL_ALIASES } from './model-aliases.ts';
|
|
|
23
23
|
|
|
24
24
|
const DIRECTIVE_RE = /^\s*\[([^\]]*)\]\s*([\s\S]*)/;
|
|
25
25
|
|
|
26
|
+
const DIRECTIVE_KEYS = ['model', 'turns', 'thinking', 'think', 'effort', 'engine'];
|
|
27
|
+
|
|
28
|
+
/** Does a bracket group look like directives (vs. prompt text that happens to start with `[`)?
|
|
29
|
+
* Every token must be a known key:value or a known positional, else we leave the group alone. */
|
|
30
|
+
function isDirectiveGroup(raw: string): boolean {
|
|
31
|
+
const tokens = raw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
32
|
+
if (!tokens.length) return false;
|
|
33
|
+
return tokens.every((t) => {
|
|
34
|
+
const colonIdx = t.indexOf(':');
|
|
35
|
+
if (colonIdx !== -1) return DIRECTIVE_KEYS.includes(t.slice(0, colonIdx).trim().toLowerCase());
|
|
36
|
+
const v = t.toLowerCase();
|
|
37
|
+
return !!MODEL_ALIASES[v] || /^\d+$/.test(v) || ['think', 'adaptive', 'nothink', 'nothinking'].includes(v);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export function parseDirectives(text: string): ParsedPrompt {
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// Consume EVERY consecutive leading [..] group, not just the first. runner.ts prepends
|
|
43
|
+
// `[model] ` onto prompts that may already open with `[turns:120]`, so a single-group parse
|
|
44
|
+
// silently dropped the second — a schedule pinned to opus quietly ran on the config default
|
|
45
|
+
// for three days. Groups that don't parse as directives are left as prompt text.
|
|
46
|
+
let rest = text;
|
|
47
|
+
const groups: string[] = [];
|
|
48
|
+
for (;;) {
|
|
49
|
+
const m = rest.match(DIRECTIVE_RE);
|
|
50
|
+
if (!m) break;
|
|
51
|
+
const g = m[1].trim();
|
|
52
|
+
// The first group is always consumed (long-standing contract: `[unknown] hi` strips and warns).
|
|
53
|
+
// Later groups must actually look like directives, so prompt text such as `[WARN] …` survives.
|
|
54
|
+
if (groups.length && g && !isDirectiveGroup(g)) break;
|
|
55
|
+
groups.push(g);
|
|
56
|
+
rest = m[2];
|
|
57
|
+
}
|
|
58
|
+
if (!groups.length) return { prompt: text, directives: {} };
|
|
29
59
|
|
|
30
|
-
const raw =
|
|
31
|
-
const prompt =
|
|
60
|
+
const raw = groups.filter(Boolean).join(',');
|
|
61
|
+
const prompt = rest.trim();
|
|
32
62
|
if (!raw) return { prompt, directives: {} };
|
|
33
63
|
|
|
34
64
|
const directives: Directives = {};
|
|
@@ -19,6 +19,7 @@ const IMMUTABLE_SYSTEM_PROMPT = readFileSync(path.resolve(import.meta.dirname, '
|
|
|
19
19
|
const DEFAULT_USER_PROMPT = `You are a helpful assistant with access to MCP tools.`;
|
|
20
20
|
const DEFAULT_ALLOWED_TOOLS = ['Read', 'Edit', 'Bash', 'WebSearch', 'Glob', 'LS', 'ToolSearch'];
|
|
21
21
|
const BG_TASK_MAX_WAIT_MS = 15 * 60_000;
|
|
22
|
+
const BG_HEARTBEAT_MS = 60_000;
|
|
22
23
|
const HISTORY_LIMIT = 50;
|
|
23
24
|
|
|
24
25
|
const NO_INTERACTIVE_ANSWER = 'No interactive channel is available to answer right now. Use your best judgement to proceed, and surface these options to the user in your reply so they can redirect if needed.';
|
|
@@ -313,7 +314,8 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
313
314
|
let waitingForBg = false;
|
|
314
315
|
let bgTimer: Promise<'__bgtimeout'> | null = null;
|
|
315
316
|
let bgTimerHandle: ReturnType<typeof setTimeout> | null = null;
|
|
316
|
-
|
|
317
|
+
let bgHeartbeat: ReturnType<typeof setInterval> | null = null;
|
|
318
|
+
const clearBgTimer = () => { if (bgTimerHandle) { clearTimeout(bgTimerHandle); bgTimerHandle = null; } bgTimer = null; if (bgHeartbeat) { clearInterval(bgHeartbeat); bgHeartbeat = null; } };
|
|
317
319
|
|
|
318
320
|
try {
|
|
319
321
|
while (true) {
|
|
@@ -395,7 +397,14 @@ export class ClaudeCodeEngine implements AgentEngine {
|
|
|
395
397
|
console.log(`[claude] Result: subtype=${m.subtype}→${sub} session=${lastSessionId} turns=${sdkTurns}/${maxTurns} cost=$${m.total_cost_usd?.toFixed(4) ?? '?'} msgs=${messageCount} deltas=${textDeltaCount} (${elapsed()})`);
|
|
396
398
|
logCacheUsage(m.usage, activeModel);
|
|
397
399
|
if (outstandingTasks.size > 0) {
|
|
398
|
-
if (!waitingForBg) {
|
|
400
|
+
if (!waitingForBg) {
|
|
401
|
+
waitingForBg = true; clearBgTimer();
|
|
402
|
+
console.log(`[claude] Holding stream for ${outstandingTasks.size} bg tasks (${elapsed()})`);
|
|
403
|
+
// The hold is invisible to the user (their next message queues behind it), so beat
|
|
404
|
+
// every minute — otherwise a stuck task reads as a dead agent with nothing in the log.
|
|
405
|
+
bgHeartbeat = setInterval(() => console.log(`[claude] Still holding for ${outstandingTasks.size} bg task(s): ${[...outstandingTasks].join(',')} (${elapsed()})`), BG_HEARTBEAT_MS);
|
|
406
|
+
bgHeartbeat.unref?.();
|
|
407
|
+
}
|
|
399
408
|
continue;
|
|
400
409
|
}
|
|
401
410
|
// The SDK reports `subtype: 'success'` even when the API call failed (e.g. an org spend
|