shraga 0.0.3 → 0.1.2
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/README.md +82 -27
- package/defaults/agents/summarizer.md +16 -0
- package/defaults/agents/trace-extractor.md +84 -0
- package/defaults/bin/claude +45 -0
- package/defaults/bin/claude-revive +17 -0
- package/defaults/extensions/README.md +70 -0
- package/defaults/extensions/selftest.ext.ts +43 -0
- package/defaults/extensions/stripe-webhook.ext.ts +58 -0
- package/defaults/gmail-triage-prompt.md +42 -0
- package/defaults/scripts/README +4 -0
- package/defaults/scripts/agent-once.ts +67 -0
- package/defaults/scripts/backfill-slack-usernames.ts +82 -0
- package/defaults/scripts/notifier-throttle.ts +44 -0
- package/defaults/scripts/summarize-conversations.ts +5 -0
- package/defaults/shraga.config.ts +29 -0
- package/defaults/skills/add-skill.md +14 -0
- package/defaults/skills/artifacts.md +116 -0
- package/defaults/skills/code-review.md +26 -0
- package/defaults/skills/communications.md +54 -0
- package/defaults/skills/context-audit.md +87 -0
- package/defaults/skills/debug.md +10 -0
- package/defaults/skills/garden.md +179 -0
- package/defaults/skills/github-contributor.md +35 -0
- package/defaults/skills/identity.md +30 -0
- package/defaults/skills/mcp-server.md +62 -0
- package/defaults/skills/mcps-sync.md +105 -0
- package/defaults/skills/plan.md +9 -0
- package/defaults/skills/platform.md +177 -0
- package/defaults/skills/reconcile.md +239 -0
- package/defaults/skills/scheduler.md +192 -0
- package/defaults/skills/self-aware.md +136 -0
- package/defaults/skills/shraga-know.md +333 -0
- package/defaults/skills/stripe.md +55 -0
- package/defaults/skills/write-tests.md +10 -0
- package/defaults/skills-defaults.json +1 -0
- package/defaults/system-prompt.md +46 -0
- package/defaults/workspace/context.md +28 -0
- package/defaults/workspace.md +50 -0
- package/defaults/zdotdir/.gitignore +8 -0
- package/defaults/zdotdir/.zlogin +3 -0
- package/defaults/zdotdir/.zprofile +1 -0
- package/defaults/zdotdir/.zshenv +4 -0
- package/defaults/zdotdir/.zshrc +3 -0
- package/dist/client/assets/index-BoHttkMt.js +1940 -0
- package/dist/client/assets/index-DdibEb2O.css +10 -0
- package/dist/client/index.html +22 -0
- package/package.json +59 -14
- package/src/cli.ts +71 -46
- package/src/client/App.tsx +510 -0
- package/src/client/components/ArtifactCard.tsx +26 -0
- package/src/client/components/ArtifactPanel.tsx +138 -0
- package/src/client/components/AuthedImage.tsx +85 -0
- package/src/client/components/AutocompleteTextarea.tsx +149 -0
- package/src/client/components/ChatView.tsx +866 -0
- package/src/client/components/CliAuthConsent.tsx +98 -0
- package/src/client/components/ConfigPanel.tsx +328 -0
- package/src/client/components/ConversationHeader.tsx +156 -0
- package/src/client/components/ConversationPane.tsx +277 -0
- package/src/client/components/LoginPage.tsx +81 -0
- package/src/client/components/MachineStats.tsx +77 -0
- package/src/client/components/McpManager.tsx +209 -0
- package/src/client/components/MessageInput.tsx +263 -0
- package/src/client/components/OAuthConsent.tsx +103 -0
- package/src/client/components/SchedulesManager.tsx +99 -0
- package/src/client/components/Sidebar.tsx +235 -0
- package/src/client/components/SkillsManager.tsx +280 -0
- package/src/client/components/SmartChart.tsx +167 -0
- package/src/client/components/Toast.tsx +54 -0
- package/src/client/components/WorkspaceTree.tsx +313 -0
- package/src/client/components/ZoomableImage.tsx +123 -0
- package/src/client/components/artifact-presets.ts +10 -0
- package/src/client/components/schedules/ScheduleEditor.tsx +264 -0
- package/src/client/components/schedules/ScheduleList.tsx +271 -0
- package/src/client/components/ui/accordion.tsx +50 -0
- package/src/client/components/ui/button.tsx +43 -0
- package/src/client/components/ui/dialog.tsx +82 -0
- package/src/client/components/ui/input.tsx +19 -0
- package/src/client/components/ui/scroll-area.tsx +39 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/globals.css +51 -0
- package/src/client/hooks/useAgentSocket.ts +79 -0
- package/src/client/hooks/useArtifacts.ts +89 -0
- package/src/client/hooks/useAuth.ts +127 -0
- package/src/client/hooks/useConversation.ts +412 -0
- package/src/client/hooks/useDarkMode.ts +57 -0
- package/src/client/hooks/useIsMobile.ts +23 -0
- package/src/client/hooks/usePush.ts +127 -0
- package/src/client/hooks/useSchedules.ts +73 -0
- package/src/client/hooks/useUnread.ts +238 -0
- package/src/client/lib/desktopAttention.ts +75 -0
- package/src/client/lib/firebase.ts +32 -0
- package/src/client/lib/googleAuthNative.ts +94 -0
- package/src/client/lib/native.ts +43 -0
- package/src/client/lib/schedule-types.ts +34 -0
- package/src/client/lib/sessionApi.ts +58 -0
- package/src/client/lib/slots.tsx +79 -0
- package/src/client/lib/storage.ts +39 -0
- package/src/client/lib/utils.ts +26 -0
- package/src/client/lib/workspaceContext.tsx +54 -0
- package/src/client/lib/ws.ts +203 -0
- package/src/client/main.tsx +14 -0
- package/src/mcp-stdio-bridge.ts +70 -0
- package/src/scripts/summarize-conversations.ts +5 -0
- package/src/scripts/typecheck.ts +43 -0
- package/src/server/agents.ts +54 -0
- package/src/server/api-keys.ts +63 -0
- package/src/server/artifacts/artifacts.export.ts +85 -0
- package/src/server/artifacts/artifacts.handler.ts +93 -0
- package/src/server/artifacts/artifacts.routes.ts +43 -0
- package/src/server/artifacts/artifacts.service.ts +100 -0
- package/src/server/artifacts/artifacts.types.ts +31 -0
- package/src/server/auth.ts +262 -0
- package/src/server/claude.ts +394 -0
- package/src/server/commands.ts +21 -0
- package/src/server/contacts.ts +177 -0
- package/src/server/conversation-summarizer.ts +204 -0
- package/src/server/data-sync.ts +664 -0
- package/src/server/directives.ts +91 -0
- package/src/server/engine/claude-code.ts +514 -0
- package/src/server/engine/index.ts +41 -0
- package/src/server/engine/registry.ts +21 -0
- package/src/server/engine/shared.ts +47 -0
- package/src/server/engine/types.ts +48 -0
- package/src/server/env-resolve.ts +71 -0
- package/src/server/env-sanitize.ts +9 -0
- package/src/server/events/bus.ts +29 -0
- package/src/server/events/dispatcher.ts +48 -0
- package/src/server/events/routes.ts +19 -0
- package/src/server/events/types.ts +9 -0
- package/src/server/extensions.ts +101 -0
- package/src/server/features.ts +109 -0
- package/src/server/file-inject.ts +45 -0
- package/src/server/hooks.ts +142 -0
- package/src/server/idempotency.ts +25 -0
- package/src/server/index.ts +1715 -0
- package/src/server/integrity-audit.ts +132 -0
- package/src/server/mcp-catalog.ts +70 -0
- package/src/server/mcp-oauth.ts +198 -0
- package/src/server/mcp-progress.ts +45 -0
- package/src/server/mcp-server.ts +456 -0
- package/src/server/mcp-sidecar.ts +87 -0
- package/src/server/mcp.ts +291 -0
- package/src/server/model-aliases.ts +76 -0
- package/src/server/paths.ts +24 -0
- package/src/server/polls.ts +175 -0
- package/src/server/push/apns.ts +113 -0
- package/src/server/push/fcm.ts +108 -0
- package/src/server/push/push.ts +66 -0
- package/src/server/push/store.ts +84 -0
- package/src/server/push/triggers.ts +99 -0
- package/src/server/scheduler/builtins.ts +157 -0
- package/src/server/scheduler/engine.ts +432 -0
- package/src/server/scheduler/index.ts +4 -0
- package/src/server/scheduler/runner.ts +334 -0
- package/src/server/scheduler/storage.ts +98 -0
- package/src/server/scheduler/timing.ts +70 -0
- package/src/server/scheduler/types.ts +62 -0
- package/src/server/sdk-utils.ts +45 -0
- package/src/server/seed.ts +174 -0
- package/src/server/session-bus.ts +18 -0
- package/src/server/sessions.ts +559 -0
- package/src/server/shraga-config.ts +167 -0
- package/src/server/skills.ts +372 -0
- package/src/server/slack/api.ts +37 -0
- package/src/server/slack/bot.ts +391 -0
- package/src/server/slack/context-cache.ts +42 -0
- package/src/server/slack/feature.ts +59 -0
- package/src/server/slack/mention-rewrite.ts +59 -0
- package/src/server/slack/oauth.ts +102 -0
- package/src/server/slack/questions.ts +112 -0
- package/src/server/slack/sessions.ts +139 -0
- package/src/server/stats.ts +106 -0
- package/src/server/summarize.ts +11 -0
- package/src/server/turn-context.ts +61 -0
- package/src/server/unclaw-config.ts +19 -0
- package/src/server/unread.ts +79 -0
- package/src/server/user-context.ts +33 -0
- package/src/server/vendor-sync.ts +52 -0
- package/src/server/voice-provider.ts +74 -0
- package/src/server/workspace.ts +249 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Voice-provider seam — the engine-level drop-in for the optional duplex (voice) brain.
|
|
2
|
+
//
|
|
3
|
+
// An add-on engine supports a "voice" turn: when a turn requests the voice channel it hands off to a
|
|
4
|
+
// REFLEX/ACT/THINK duplex agent instead of the normal single agent. The engine knows HOW to drive such
|
|
5
|
+
// a session (send a turn, cancel background workers, wait for idle) but the seam keeps CE from having
|
|
6
|
+
// to know how one is BUILT — the duplex agent, its reflex/think model choices, and its voice runtime
|
|
7
|
+
// import all belong to an optional add-on, not to CE.
|
|
8
|
+
//
|
|
9
|
+
// So the core owns only this seam contract: when a turn is in voice mode, the engine calls the
|
|
10
|
+
// registered provider to build an opaque duplex session and then drives it. CE registers NOTHING here
|
|
11
|
+
// — with no provider, voice mode is a silent no-op (the turn runs as a normal agent turn). An add-on
|
|
12
|
+
// (shraga-ee) registers the provider that constructs the duplex agent and resolves the reflex/think
|
|
13
|
+
// models off the opaque `directives`/`config` bags (whose voice keys CE no longer names).
|
|
14
|
+
//
|
|
15
|
+
// Sibling of `features.ts` / `turn-context.ts` and shaped like them: typed, optional, registered
|
|
16
|
+
// before startup, idempotent, a no-op when nothing registers.
|
|
17
|
+
|
|
18
|
+
/** Everything the core has assembled for the turn that a duplex builder needs. `directives`/`config`
|
|
19
|
+
* are passed opaquely — the provider reads its own voice keys (e.g. `voiceModel`/`thinkModel`) off
|
|
20
|
+
* them; the core never names them. */
|
|
21
|
+
export interface VoiceBuildInput {
|
|
22
|
+
/** The provider AI client (opaque to core — the add-on that registers the provider knows its type). */
|
|
23
|
+
ai: unknown;
|
|
24
|
+
/** The project-rooted filesystem the provider uses (opaque to core). */
|
|
25
|
+
fs: unknown;
|
|
26
|
+
/** The turn's host (notify/ask/confirm) — the duplex agent emits its worker/reflex events through it. */
|
|
27
|
+
host: unknown;
|
|
28
|
+
/** The picked model = the Act tier. */
|
|
29
|
+
actModel: string;
|
|
30
|
+
/** Opaque per-session directives bag — the provider reads its own voice keys. */
|
|
31
|
+
directives: Record<string, unknown>;
|
|
32
|
+
/** Opaque persisted agent config bag — the provider reads its own voice keys. */
|
|
33
|
+
config: Record<string, unknown>;
|
|
34
|
+
/** Assembled tool set for the Act/Think workers. */
|
|
35
|
+
tools: unknown[];
|
|
36
|
+
/** Security hooks for the workers. */
|
|
37
|
+
hooks: unknown;
|
|
38
|
+
/** System prompt the workers carry (shraga's real prompt). */
|
|
39
|
+
systemPrompt: string;
|
|
40
|
+
/** Max agent steps for the workers. */
|
|
41
|
+
maxSteps: number;
|
|
42
|
+
/** Subagents directory, if any. */
|
|
43
|
+
agentsDir?: string;
|
|
44
|
+
/** Turn abort signal (reflex + workers). */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
/** Connected MCP server names (for the reflex's capability quick-look). */
|
|
47
|
+
mcpServerNames: string[];
|
|
48
|
+
/** Project root (for cursor warm-session provider options). */
|
|
49
|
+
projectRoot: string;
|
|
50
|
+
/** Chat session id (for cursor warm-session keying). */
|
|
51
|
+
sessionId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build an opaque duplex session for a voice turn. The return is driven by the core's run loop
|
|
56
|
+
* (`send(prompt)`, background-worker cancel via `tasks`/`cancelTask`, `idle()`) — its concrete shape
|
|
57
|
+
* is the add-on's `DuplexAgent`, kept opaque here so the core names none of it.
|
|
58
|
+
*/
|
|
59
|
+
export type VoiceProvider = (input: VoiceBuildInput) => Promise<unknown> | unknown;
|
|
60
|
+
|
|
61
|
+
let provider: VoiceProvider | null = null;
|
|
62
|
+
|
|
63
|
+
/** Register the voice provider. Called by an optional add-on before startup. First registration wins
|
|
64
|
+
* (idempotent), matching the other seams. */
|
|
65
|
+
export function registerVoiceProvider(p: VoiceProvider): void {
|
|
66
|
+
if (provider) return;
|
|
67
|
+
provider = p;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The registered voice provider, or null when none is registered (CE's own state → voice mode is a
|
|
71
|
+
* no-op and the turn runs as a normal agent turn). */
|
|
72
|
+
export function getVoiceProvider(): VoiceProvider | null {
|
|
73
|
+
return provider;
|
|
74
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, existsSync, watch } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { dataPath } from './paths.ts';
|
|
4
|
+
import { dataSync } from './data-sync.ts';
|
|
5
|
+
import { injectFile } from './file-inject.ts';
|
|
6
|
+
|
|
7
|
+
export const WORKSPACE_DIR = process.env.WORKSPACE_DIR || dataPath('workspace');
|
|
8
|
+
|
|
9
|
+
const BINARY_EXTS = new Set([
|
|
10
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.avif', '.tiff',
|
|
11
|
+
'.mp4', '.webm', '.avi', '.mov', '.mkv', '.mp3', '.ogg', '.wav', '.flac', '.aac',
|
|
12
|
+
'.pdf', '.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
|
|
13
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a',
|
|
14
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
15
|
+
'.sqlite', '.db', '.sqlite3',
|
|
16
|
+
]);
|
|
17
|
+
const MAX_CONTEXT_BYTES = 5_000;
|
|
18
|
+
const MAX_ENTRIES = 500;
|
|
19
|
+
|
|
20
|
+
export interface WorkspaceEntry {
|
|
21
|
+
path: string; // relative to WORKSPACE_DIR, POSIX separators
|
|
22
|
+
type: 'file' | 'dir';
|
|
23
|
+
size?: number;
|
|
24
|
+
oneLiner?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ensureDir() {
|
|
28
|
+
mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isTextFile(p: string): boolean {
|
|
32
|
+
const ext = path.extname(p).toLowerCase();
|
|
33
|
+
if (BINARY_EXTS.has(ext)) return false;
|
|
34
|
+
if (ext) return true;
|
|
35
|
+
try {
|
|
36
|
+
const buf = readFileSync(p, { flag: 'r' });
|
|
37
|
+
const sample = buf.subarray(0, Math.min(buf.length, 512));
|
|
38
|
+
return !sample.some(b => b === 0);
|
|
39
|
+
} catch { return false; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function safeResolve(relPath: string): string | null {
|
|
43
|
+
const resolved = path.resolve(WORKSPACE_DIR, relPath);
|
|
44
|
+
const base = path.resolve(WORKSPACE_DIR);
|
|
45
|
+
if (resolved !== base && !resolved.startsWith(base + path.sep)) return null;
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function firstMeaningfulLine(content: string): string {
|
|
50
|
+
for (const raw of content.split('\n')) {
|
|
51
|
+
const line = raw.trim().replace(/^#+\s*/, '').replace(/^[-*>]\s*/, '');
|
|
52
|
+
if (line) return line.slice(0, 100);
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function listWorkspaceTree(): WorkspaceEntry[] {
|
|
58
|
+
ensureDir();
|
|
59
|
+
const entries: WorkspaceEntry[] = [];
|
|
60
|
+
const walk = (dir: string, rel: string) => {
|
|
61
|
+
if (entries.length >= MAX_ENTRIES) return;
|
|
62
|
+
let items: import('node:fs').Dirent[];
|
|
63
|
+
try { items = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
64
|
+
items.sort((a, b) => {
|
|
65
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
66
|
+
return a.name.localeCompare(b.name);
|
|
67
|
+
});
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
if (item.name.startsWith('.')) continue;
|
|
70
|
+
const full = path.join(dir, item.name);
|
|
71
|
+
const relPath = rel ? `${rel}/${item.name}` : item.name;
|
|
72
|
+
if (item.isDirectory()) {
|
|
73
|
+
entries.push({ path: relPath, type: 'dir' });
|
|
74
|
+
walk(full, relPath);
|
|
75
|
+
} else if (item.isFile()) {
|
|
76
|
+
let size = 0;
|
|
77
|
+
try { size = statSync(full).size; } catch {}
|
|
78
|
+
let oneLiner: string | undefined;
|
|
79
|
+
if (isTextFile(full) && size < 64_000) {
|
|
80
|
+
try { oneLiner = firstMeaningfulLine(readFileSync(full, 'utf-8')); } catch {}
|
|
81
|
+
}
|
|
82
|
+
entries.push({ path: relPath, type: 'file', size, oneLiner });
|
|
83
|
+
}
|
|
84
|
+
if (entries.length >= MAX_ENTRIES) return;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(WORKSPACE_DIR, '');
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function listWorkspaceDir(relDir = ''): WorkspaceEntry[] {
|
|
92
|
+
ensureDir();
|
|
93
|
+
const base = relDir ? safeResolve(relDir) : WORKSPACE_DIR;
|
|
94
|
+
if (!base) return [];
|
|
95
|
+
let items: import('node:fs').Dirent[];
|
|
96
|
+
try { items = readdirSync(base, { withFileTypes: true }); } catch { return []; }
|
|
97
|
+
items.sort((a, b) => {
|
|
98
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
99
|
+
return a.name.localeCompare(b.name);
|
|
100
|
+
});
|
|
101
|
+
const entries: WorkspaceEntry[] = [];
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
if (item.name.startsWith('.')) continue;
|
|
104
|
+
const full = path.join(base, item.name);
|
|
105
|
+
const relPath = relDir ? `${relDir}/${item.name}` : item.name;
|
|
106
|
+
if (item.isDirectory()) {
|
|
107
|
+
entries.push({ path: relPath, type: 'dir' });
|
|
108
|
+
} else if (item.isFile()) {
|
|
109
|
+
let size = 0;
|
|
110
|
+
try { size = statSync(full).size; } catch {}
|
|
111
|
+
let oneLiner: string | undefined;
|
|
112
|
+
if (isTextFile(full) && size < 64_000) {
|
|
113
|
+
try { oneLiner = firstMeaningfulLine(readFileSync(full, 'utf-8')); } catch {}
|
|
114
|
+
}
|
|
115
|
+
entries.push({ path: relPath, type: 'file', size, oneLiner });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function listWorkspaceFilePaths(): string[] {
|
|
122
|
+
return listWorkspaceTree().filter((e) => e.type === 'file').map((e) => e.path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SearchMatch {
|
|
126
|
+
path: string;
|
|
127
|
+
line: number;
|
|
128
|
+
text: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function searchWorkspace(query: string, maxResults = 50): SearchMatch[] {
|
|
132
|
+
ensureDir();
|
|
133
|
+
const lower = query.toLowerCase();
|
|
134
|
+
const matches: SearchMatch[] = [];
|
|
135
|
+
const files = listWorkspaceTree().filter(e => e.type === 'file');
|
|
136
|
+
for (const entry of files) {
|
|
137
|
+
if (matches.length >= maxResults) break;
|
|
138
|
+
const resolved = safeResolve(entry.path);
|
|
139
|
+
if (!resolved || !isTextFile(resolved)) continue;
|
|
140
|
+
let content: string;
|
|
141
|
+
try {
|
|
142
|
+
const st = statSync(resolved);
|
|
143
|
+
if (st.size > 256_000) continue;
|
|
144
|
+
content = readFileSync(resolved, 'utf-8');
|
|
145
|
+
} catch { continue; }
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
if (matches.length >= maxResults) break;
|
|
149
|
+
if (lines[i].toLowerCase().includes(lower)) {
|
|
150
|
+
matches.push({ path: entry.path, line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return matches;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function readWorkspaceFile(relPath: string): { content: string; binary: boolean } | null {
|
|
158
|
+
const resolved = safeResolve(relPath);
|
|
159
|
+
if (!resolved || !existsSync(resolved)) return null;
|
|
160
|
+
const st = statSync(resolved);
|
|
161
|
+
if (!st.isFile()) return null;
|
|
162
|
+
if (!isTextFile(resolved)) return { content: '', binary: true };
|
|
163
|
+
return { content: readFileSync(resolved, 'utf-8'), binary: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildWorkspaceContextBlock(): string {
|
|
167
|
+
const entries = listWorkspaceTree();
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
let bytes = 0;
|
|
170
|
+
for (const e of entries) {
|
|
171
|
+
const line = e.type === 'dir'
|
|
172
|
+
? `${e.path}/`
|
|
173
|
+
: e.oneLiner
|
|
174
|
+
? `${e.path} — ${e.oneLiner}`
|
|
175
|
+
: e.path;
|
|
176
|
+
if (bytes + line.length + 1 > MAX_CONTEXT_BYTES) {
|
|
177
|
+
lines.push('… (truncated)');
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
lines.push(line);
|
|
181
|
+
bytes += line.length + 1;
|
|
182
|
+
}
|
|
183
|
+
return [
|
|
184
|
+
'<workspace>',
|
|
185
|
+
`This is the team's shared knowledge folder — living context: business info, ongoing tasks, research dumps, kanban lists, decisions. You are EXPECTED to read, write, edit, and reorganize files here as part of normal work. When the user mentions "the workspace", "our notes", "our tasks", "kanban", etc., they mean this folder.`,
|
|
186
|
+
'',
|
|
187
|
+
`LOCATION: ${WORKSPACE_DIR}`,
|
|
188
|
+
`Always use this absolute path with Read/Write/Edit/Glob — NOT "/workspace" (root) and NOT a project-relative path. Example: Write { file_path: "${WORKSPACE_DIR}/tasks.md", content: "..." }.`,
|
|
189
|
+
'',
|
|
190
|
+
'Files explicitly @-mentioned by the user are already inlined below as <workspace-file> blocks. For anything else in the tree, read on demand.',
|
|
191
|
+
'',
|
|
192
|
+
entries.length === 0
|
|
193
|
+
? 'Tree: (empty — feel free to create initial files like tasks.md, context.md, research/, etc.)'
|
|
194
|
+
: 'Tree (paths shown relative to workspace root):',
|
|
195
|
+
...lines.map((l) => ` ${l}`),
|
|
196
|
+
'',
|
|
197
|
+
injectFile(path.join(WORKSPACE_DIR, 'context.md'), { label: 'workspace-context', maxChars: 0 }),
|
|
198
|
+
'</workspace>',
|
|
199
|
+
].filter(Boolean).join('\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const WORKSPACE_MENTION_RE = /@([a-zA-Z0-9_./-]+\.[a-zA-Z0-9]+)/g;
|
|
203
|
+
|
|
204
|
+
export function expandWorkspaceMentions(text: string): string {
|
|
205
|
+
const files = new Set(listWorkspaceFilePaths());
|
|
206
|
+
const matched = new Set<string>();
|
|
207
|
+
for (const m of text.matchAll(WORKSPACE_MENTION_RE)) {
|
|
208
|
+
if (files.has(m[1])) matched.add(m[1]);
|
|
209
|
+
}
|
|
210
|
+
if (matched.size === 0) return text;
|
|
211
|
+
const blocks: string[] = [];
|
|
212
|
+
for (const rel of matched) {
|
|
213
|
+
const file = readWorkspaceFile(rel);
|
|
214
|
+
if (!file || file.binary) continue;
|
|
215
|
+
blocks.push(`<workspace-file path="${rel}">\n${file.content}\n</workspace-file>`);
|
|
216
|
+
}
|
|
217
|
+
if (blocks.length === 0) return text;
|
|
218
|
+
return `${blocks.join('\n')}\n\n${text}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function watchWorkspace(cb: (event: { action: 'created' | 'modified' | 'deleted'; path: string }) => void): () => void {
|
|
222
|
+
ensureDir();
|
|
223
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
224
|
+
const pending = new Map<string, 'created' | 'modified' | 'deleted'>();
|
|
225
|
+
const flush = () => {
|
|
226
|
+
for (const [p, action] of pending) cb({ action, path: p });
|
|
227
|
+
pending.clear();
|
|
228
|
+
timer = null;
|
|
229
|
+
};
|
|
230
|
+
let watcher: import('node:fs').FSWatcher;
|
|
231
|
+
try {
|
|
232
|
+
watcher = watch(WORKSPACE_DIR, { recursive: true }, (eventType, filename) => {
|
|
233
|
+
if (!filename) return;
|
|
234
|
+
const rel = String(filename).split(path.sep).join('/');
|
|
235
|
+
if (rel.split('/').some((seg) => seg.startsWith('.'))) return;
|
|
236
|
+
const full = path.join(WORKSPACE_DIR, rel);
|
|
237
|
+
const action: 'created' | 'modified' | 'deleted' = existsSync(full)
|
|
238
|
+
? (eventType === 'rename' ? 'created' : 'modified')
|
|
239
|
+
: 'deleted';
|
|
240
|
+
pending.set(rel, action);
|
|
241
|
+
dataSync.trackWrite(`workspace/${rel}`);
|
|
242
|
+
if (!timer) timer = setTimeout(flush, 300);
|
|
243
|
+
});
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.warn('[workspace] watch failed:', (err as Error).message);
|
|
246
|
+
return () => {};
|
|
247
|
+
}
|
|
248
|
+
return () => watcher.close();
|
|
249
|
+
}
|