@workscribe/cli 1.12.0 → 1.13.0
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/dist/index.js +621 -167
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/shell/hook.bash +3 -0
- package/shell/hook.fish +3 -0
- package/shell/hook.zsh +3 -0
package/dist/index.js
CHANGED
|
@@ -27,15 +27,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// src/utils/node-version.ts
|
|
30
|
+
function detectVersionManager() {
|
|
31
|
+
if (process.env.NVM_DIR) return "nvm";
|
|
32
|
+
if (process.env.VOLTA_HOME) return "volta";
|
|
33
|
+
if (process.env.FNM_DIR) return "fnm";
|
|
34
|
+
if (process.env.MISE_DATA_DIR || process.env.MISE_SHELL) return "mise";
|
|
35
|
+
if (process.env.ASDF_DIR || process.env.ASDF_DATA_DIR) return "asdf";
|
|
36
|
+
if (process.env.NODENV_ROOT) return "nodenv";
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
var UPGRADE_COMMANDS = {
|
|
40
|
+
nvm: "nvm install 22 && nvm use 22",
|
|
41
|
+
volta: "volta install node@22",
|
|
42
|
+
fnm: "fnm install 22 && fnm use 22",
|
|
43
|
+
mise: "mise use --global node@22",
|
|
44
|
+
asdf: "asdf install nodejs 22 && asdf global nodejs 22",
|
|
45
|
+
nodenv: "nodenv install 22 && nodenv global 22"
|
|
46
|
+
};
|
|
30
47
|
function assertNodeVersion(nodeVersion) {
|
|
31
48
|
const major = Number(nodeVersion.split(".")[0]);
|
|
32
49
|
if (major < 22) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
const manager = detectVersionManager();
|
|
51
|
+
const upgradeHint = manager ? `Upgrade with ${manager}:
|
|
52
|
+
${UPGRADE_COMMANDS[manager]}` : "Download the latest version from https://nodejs.org";
|
|
53
|
+
process.stderr.write(`workscribe requires Node.js 22 or later (you have v${nodeVersion}).
|
|
54
|
+
|
|
55
|
+
${upgradeHint}
|
|
56
|
+
`);
|
|
39
57
|
process.exit(1);
|
|
40
58
|
}
|
|
41
59
|
}
|
|
@@ -56,7 +74,7 @@ var USE_XDG = !import_fs.default.existsSync(LEGACY_DIR);
|
|
|
56
74
|
var xdgDataDir = USE_XDG ? import_path.default.join(process.env.XDG_DATA_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "share"), "workscribe") : LEGACY_DIR;
|
|
57
75
|
var xdgConfigDir = USE_XDG ? import_path.default.join(process.env.XDG_CONFIG_HOME ?? import_path.default.join(import_os.default.homedir(), ".config"), "workscribe") : LEGACY_DIR;
|
|
58
76
|
var xdgStateDir = USE_XDG ? import_path.default.join(process.env.XDG_STATE_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "state"), "workscribe") : LEGACY_DIR;
|
|
59
|
-
var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir])];
|
|
77
|
+
var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir, LEGACY_DIR])];
|
|
60
78
|
var WORKSCRIBE_DIR = xdgDataDir;
|
|
61
79
|
var DB_PATH = import_path.default.join(xdgDataDir, "workscribe.db");
|
|
62
80
|
var CONFIG_PATH = import_path.default.join(xdgConfigDir, "config.json");
|
|
@@ -66,6 +84,7 @@ var EXPORTS_DIR = import_path.default.join(xdgDataDir, "exports");
|
|
|
66
84
|
var PAUSE_FLAG_PATH = import_path.default.join(xdgStateDir, "paused");
|
|
67
85
|
var WEEKLY_CACHE_DIR = import_path.default.join(xdgDataDir, "weekly-cache");
|
|
68
86
|
var PROFILES_DIR = import_path.default.join(xdgDataDir, "profiles");
|
|
87
|
+
var ALERTS_PENDING_PATH = import_path.default.join(LEGACY_DIR, "alerts.pending");
|
|
69
88
|
|
|
70
89
|
// src/db/migrations/001_initial_schema.ts
|
|
71
90
|
var initial_schema_default = {
|
|
@@ -127,8 +146,38 @@ var migration = {
|
|
|
127
146
|
};
|
|
128
147
|
var semantic_categories_default = migration;
|
|
129
148
|
|
|
149
|
+
// src/db/migrations/003_alerts.ts
|
|
150
|
+
var migration2 = {
|
|
151
|
+
version: 3,
|
|
152
|
+
description: "Add alerts table for AI session secret detection",
|
|
153
|
+
sql: `
|
|
154
|
+
CREATE TABLE IF NOT EXISTS alerts (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
created_at INTEGER NOT NULL,
|
|
157
|
+
tool TEXT NOT NULL,
|
|
158
|
+
session_id TEXT,
|
|
159
|
+
context TEXT NOT NULL,
|
|
160
|
+
cleared INTEGER NOT NULL DEFAULT 0
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS alerts_cleared ON alerts (cleared);
|
|
163
|
+
`
|
|
164
|
+
};
|
|
165
|
+
var alerts_default = migration2;
|
|
166
|
+
|
|
167
|
+
// src/db/migrations/004_ide_columns.ts
|
|
168
|
+
var migration3 = {
|
|
169
|
+
version: 4,
|
|
170
|
+
description: "Add source and file_path columns to events for IDE integration",
|
|
171
|
+
sql: `
|
|
172
|
+
ALTER TABLE events ADD COLUMN source TEXT;
|
|
173
|
+
ALTER TABLE events ADD COLUMN file_path TEXT;
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_events_file_path ON events (file_path, cwd);
|
|
175
|
+
`
|
|
176
|
+
};
|
|
177
|
+
var ide_columns_default = migration3;
|
|
178
|
+
|
|
130
179
|
// src/db/migrations/index.ts
|
|
131
|
-
var MIGRATIONS = [initial_schema_default, semantic_categories_default];
|
|
180
|
+
var MIGRATIONS = [initial_schema_default, semantic_categories_default, alerts_default, ide_columns_default];
|
|
132
181
|
|
|
133
182
|
// src/db/index.ts
|
|
134
183
|
var _db = null;
|
|
@@ -165,13 +214,13 @@ function applyMigrations(db) {
|
|
|
165
214
|
const applied = new Set(
|
|
166
215
|
db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
|
|
167
216
|
);
|
|
168
|
-
for (const
|
|
169
|
-
if (applied.has(
|
|
217
|
+
for (const migration4 of MIGRATIONS) {
|
|
218
|
+
if (applied.has(migration4.version)) continue;
|
|
170
219
|
db.exec("BEGIN");
|
|
171
220
|
try {
|
|
172
|
-
db.exec(
|
|
221
|
+
db.exec(migration4.sql);
|
|
173
222
|
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
|
|
174
|
-
version:
|
|
223
|
+
version: migration4.version
|
|
175
224
|
});
|
|
176
225
|
db.exec("COMMIT");
|
|
177
226
|
} catch (err) {
|
|
@@ -179,18 +228,48 @@ function applyMigrations(db) {
|
|
|
179
228
|
db.exec("ROLLBACK");
|
|
180
229
|
} catch {
|
|
181
230
|
}
|
|
182
|
-
throw new Error(`Migration ${
|
|
231
|
+
throw new Error(`Migration ${migration4.version} (${migration4.description}) failed: ${err.message}`);
|
|
183
232
|
}
|
|
184
233
|
}
|
|
185
234
|
}
|
|
186
235
|
function insertEvent(params) {
|
|
187
236
|
const db = getDb();
|
|
188
237
|
const stmt = db.prepare(`
|
|
189
|
-
INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata)
|
|
190
|
-
VALUES ($captured_at, $command, $cwd, $exit_code, $duration_ms, $repo_name, $branch, $category, $metadata)
|
|
238
|
+
INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata, source, file_path)
|
|
239
|
+
VALUES ($captured_at, $command, $cwd, $exit_code, $duration_ms, $repo_name, $branch, $category, $metadata, $source, $file_path)
|
|
191
240
|
`);
|
|
192
241
|
stmt.run(params);
|
|
193
242
|
}
|
|
243
|
+
function getRecentSessionsByRepo(repoName, limit) {
|
|
244
|
+
const db = getDb();
|
|
245
|
+
return db.prepare("SELECT * FROM sessions WHERE repo_name = $repo_name ORDER BY started_at DESC LIMIT $limit").all({ repo_name: repoName, limit });
|
|
246
|
+
}
|
|
247
|
+
function getLastSessionTouchingFile(filePath) {
|
|
248
|
+
const db = getDb();
|
|
249
|
+
return db.prepare(
|
|
250
|
+
`SELECT s.* FROM sessions s
|
|
251
|
+
INNER JOIN session_events se ON se.session_id = s.id
|
|
252
|
+
INNER JOIN events e ON e.id = se.event_id
|
|
253
|
+
WHERE e.file_path = $file_path
|
|
254
|
+
ORDER BY s.started_at DESC
|
|
255
|
+
LIMIT 1`
|
|
256
|
+
).get({ file_path: filePath });
|
|
257
|
+
}
|
|
258
|
+
function getSessionBranch(sessionId) {
|
|
259
|
+
const db = getDb();
|
|
260
|
+
const row = db.prepare(
|
|
261
|
+
`SELECT e.branch FROM events e
|
|
262
|
+
INNER JOIN session_events se ON se.event_id = e.id
|
|
263
|
+
WHERE se.session_id = $session_id AND e.branch IS NOT NULL
|
|
264
|
+
ORDER BY e.captured_at DESC
|
|
265
|
+
LIMIT 1`
|
|
266
|
+
).get({ session_id: sessionId });
|
|
267
|
+
return row?.branch ?? null;
|
|
268
|
+
}
|
|
269
|
+
function getLastEventForFile(filePath, cwd) {
|
|
270
|
+
const db = getDb();
|
|
271
|
+
return db.prepare("SELECT * FROM events WHERE file_path = $file_path AND cwd = $cwd ORDER BY id DESC LIMIT 1").get({ file_path: filePath, cwd });
|
|
272
|
+
}
|
|
194
273
|
function getLastEvent() {
|
|
195
274
|
const db = getDb();
|
|
196
275
|
const stmt = db.prepare("SELECT * FROM events ORDER BY id DESC LIMIT 1");
|
|
@@ -332,7 +411,10 @@ var BUILTIN_PATTERNS = [
|
|
|
332
411
|
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
|
|
333
412
|
// Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
|
|
334
413
|
// Lookbehind matches only the variable-name= prefix, so only the value is replaced
|
|
335
|
-
/(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi
|
|
414
|
+
/(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi,
|
|
415
|
+
// Credentials embedded in connection string URLs (e.g. DATABASE_URL=postgres://user:pass@host/db)
|
|
416
|
+
// Matches the password segment between :// user : password @ — preserves scheme and host
|
|
417
|
+
/(?<=[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:)[^@\s]+(?=@)/g
|
|
336
418
|
];
|
|
337
419
|
function redact(command, extraPatterns = []) {
|
|
338
420
|
let result = command;
|
|
@@ -421,7 +503,7 @@ function readLastAiderPrompt(cwd) {
|
|
|
421
503
|
try {
|
|
422
504
|
const histPath = import_path3.default.join(cwd, HISTORY_FILE);
|
|
423
505
|
const content = import_fs4.default.readFileSync(histPath, "utf-8");
|
|
424
|
-
const matches = [...content.matchAll(
|
|
506
|
+
const matches = [...content.matchAll(/^#### (.+)$/gm)];
|
|
425
507
|
if (!matches.length) return null;
|
|
426
508
|
return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
|
|
427
509
|
} catch {
|
|
@@ -441,56 +523,108 @@ var ai_tools_default = [
|
|
|
441
523
|
displayName: "Claude Code",
|
|
442
524
|
pattern: "^claude\\b",
|
|
443
525
|
hook: "claude-code",
|
|
444
|
-
contextSource: "hook"
|
|
526
|
+
contextSource: "hook",
|
|
527
|
+
historyFormat: null,
|
|
528
|
+
securityWarning: null
|
|
445
529
|
},
|
|
446
530
|
{
|
|
447
531
|
tool: "aider",
|
|
448
532
|
displayName: "Aider",
|
|
449
533
|
pattern: "^aider\\b",
|
|
450
534
|
hook: null,
|
|
451
|
-
contextSource: "history-file"
|
|
535
|
+
contextSource: "history-file",
|
|
536
|
+
historyFormat: "markdown",
|
|
537
|
+
securityWarning: null
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
tool: "codex",
|
|
541
|
+
displayName: "Codex CLI",
|
|
542
|
+
pattern: "^codex\\b",
|
|
543
|
+
hook: null,
|
|
544
|
+
contextSource: "history-file",
|
|
545
|
+
historyFormat: "jsonl",
|
|
546
|
+
securityWarning: null
|
|
452
547
|
},
|
|
453
548
|
{
|
|
454
549
|
tool: "gemini",
|
|
455
|
-
displayName: "Gemini",
|
|
550
|
+
displayName: "Gemini CLI",
|
|
456
551
|
pattern: "^gemini\\b",
|
|
457
552
|
hook: null,
|
|
458
|
-
contextSource:
|
|
553
|
+
contextSource: "history-file",
|
|
554
|
+
historyFormat: "json",
|
|
555
|
+
securityWarning: null
|
|
459
556
|
},
|
|
460
557
|
{
|
|
461
558
|
tool: "gh copilot",
|
|
462
559
|
displayName: "GitHub Copilot",
|
|
463
560
|
pattern: "^gh\\s+copilot\\b",
|
|
464
561
|
hook: null,
|
|
465
|
-
contextSource:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
tool: "cody",
|
|
469
|
-
displayName: "Cody",
|
|
470
|
-
pattern: "^cody\\b",
|
|
471
|
-
hook: null,
|
|
472
|
-
contextSource: null
|
|
562
|
+
contextSource: "history-file",
|
|
563
|
+
historyFormat: "jsonl",
|
|
564
|
+
securityWarning: null
|
|
473
565
|
},
|
|
474
566
|
{
|
|
475
567
|
tool: "amazon-q",
|
|
476
568
|
displayName: "Amazon Q",
|
|
477
569
|
pattern: "^q\\s+(chat|dev)\\b",
|
|
478
570
|
hook: null,
|
|
479
|
-
contextSource:
|
|
571
|
+
contextSource: "history-file",
|
|
572
|
+
historyFormat: "json",
|
|
573
|
+
securityWarning: null
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
tool: "opencode",
|
|
577
|
+
displayName: "OpenCode",
|
|
578
|
+
pattern: "^opencode\\b",
|
|
579
|
+
hook: null,
|
|
580
|
+
contextSource: "sqlite",
|
|
581
|
+
historyFormat: "sqlite",
|
|
582
|
+
securityWarning: null
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
tool: "kilo",
|
|
586
|
+
displayName: "Kilo Code",
|
|
587
|
+
pattern: "^kilo\\b",
|
|
588
|
+
hook: null,
|
|
589
|
+
contextSource: null,
|
|
590
|
+
historyFormat: null,
|
|
591
|
+
securityWarning: null
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
tool: "grok",
|
|
595
|
+
displayName: "Grok Build",
|
|
596
|
+
pattern: "^grok\\b",
|
|
597
|
+
hook: null,
|
|
598
|
+
contextSource: null,
|
|
599
|
+
historyFormat: null,
|
|
600
|
+
securityWarning: "Grok Build was found to silently upload entire local git repositories \u2014 including .env files and untracked secrets \u2014 to Google Cloud Storage without user disclosure (July 2026 incident). Workscribe does not read Grok Build session files. Consider whether this tool is appropriate for use in repositories containing sensitive credentials."
|
|
480
601
|
},
|
|
481
602
|
{
|
|
482
603
|
tool: "continue",
|
|
483
604
|
displayName: "Continue",
|
|
484
605
|
pattern: "^continue\\b",
|
|
485
606
|
hook: null,
|
|
486
|
-
contextSource: null
|
|
607
|
+
contextSource: null,
|
|
608
|
+
historyFormat: null,
|
|
609
|
+
securityWarning: null
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
tool: "cody",
|
|
613
|
+
displayName: "Cody",
|
|
614
|
+
pattern: "^cody\\b",
|
|
615
|
+
hook: null,
|
|
616
|
+
contextSource: null,
|
|
617
|
+
historyFormat: null,
|
|
618
|
+
securityWarning: null
|
|
487
619
|
},
|
|
488
620
|
{
|
|
489
621
|
tool: "cursor",
|
|
490
622
|
displayName: "Cursor",
|
|
491
623
|
pattern: "^cursor\\b",
|
|
492
624
|
hook: null,
|
|
493
|
-
contextSource: null
|
|
625
|
+
contextSource: null,
|
|
626
|
+
historyFormat: null,
|
|
627
|
+
securityWarning: null
|
|
494
628
|
}
|
|
495
629
|
];
|
|
496
630
|
|
|
@@ -529,7 +663,9 @@ function loadAIToolRegistry() {
|
|
|
529
663
|
displayName: entry.displayName ?? entry.tool,
|
|
530
664
|
pattern: entry.pattern,
|
|
531
665
|
hook: entry.hook ?? null,
|
|
532
|
-
contextSource: entry.contextSource ?? null
|
|
666
|
+
contextSource: entry.contextSource ?? null,
|
|
667
|
+
historyFormat: entry.historyFormat ?? null,
|
|
668
|
+
securityWarning: entry.securityWarning ?? null
|
|
533
669
|
}));
|
|
534
670
|
}
|
|
535
671
|
}
|
|
@@ -1074,11 +1210,41 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
|
|
|
1074
1210
|
return lastEvent.command === normalizeForFilter(command) && lastEvent.cwd === cwd && now - lastTime < dedupWindowMs;
|
|
1075
1211
|
}
|
|
1076
1212
|
|
|
1213
|
+
// src/capture/ide.ts
|
|
1214
|
+
var IDE_DEDUP_WINDOW_MS = 3e4;
|
|
1215
|
+
|
|
1077
1216
|
// src/capture/index.ts
|
|
1078
1217
|
function captureCommand(opts) {
|
|
1079
1218
|
if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
|
|
1080
1219
|
const config = readConfig();
|
|
1081
|
-
const {
|
|
1220
|
+
const { cwd, exitCode, durationMs } = opts;
|
|
1221
|
+
if (opts.type) {
|
|
1222
|
+
if (opts.type === "code_edit" && opts.file) {
|
|
1223
|
+
const lastForFile = getLastEventForFile(opts.file, cwd);
|
|
1224
|
+
if (lastForFile) {
|
|
1225
|
+
const lastTime = new Date(lastForFile.captured_at).getTime();
|
|
1226
|
+
if (Date.now() - lastTime < IDE_DEDUP_WINDOW_MS) return;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const command2 = opts.command ?? opts.file ?? opts.type;
|
|
1230
|
+
const nameOverride2 = config.projects?.[cwd] ?? null;
|
|
1231
|
+
const { repoName: repoName2, branch: branch2 } = getGitContext(cwd, nameOverride2);
|
|
1232
|
+
insertEvent({
|
|
1233
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1234
|
+
command: command2,
|
|
1235
|
+
cwd,
|
|
1236
|
+
exit_code: exitCode,
|
|
1237
|
+
duration_ms: durationMs,
|
|
1238
|
+
repo_name: repoName2,
|
|
1239
|
+
branch: branch2,
|
|
1240
|
+
category: opts.type,
|
|
1241
|
+
metadata: null,
|
|
1242
|
+
source: opts.source ?? null,
|
|
1243
|
+
file_path: opts.file ?? null
|
|
1244
|
+
});
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const command = opts.command;
|
|
1082
1248
|
if (shouldFilter(command, config.capture.ignore)) return;
|
|
1083
1249
|
const last = getLastEvent();
|
|
1084
1250
|
if (isDuplicate(command, cwd, last ?? void 0)) return;
|
|
@@ -1092,7 +1258,6 @@ function captureCommand(opts) {
|
|
|
1092
1258
|
if (firstToken) metadata.tool = firstToken;
|
|
1093
1259
|
metadata.hint = safeCommand.trim().slice(0, 120);
|
|
1094
1260
|
}
|
|
1095
|
-
if (opts.source) metadata.source = opts.source;
|
|
1096
1261
|
if (category === "ai_session") {
|
|
1097
1262
|
if (opts.gitBefore) {
|
|
1098
1263
|
const commits = getCommitsSince(opts.gitBefore, cwd);
|
|
@@ -1115,24 +1280,30 @@ function captureCommand(opts) {
|
|
|
1115
1280
|
repo_name: repoName,
|
|
1116
1281
|
branch,
|
|
1117
1282
|
category,
|
|
1118
|
-
metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
|
|
1283
|
+
metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
|
|
1284
|
+
source: opts.source ?? null,
|
|
1285
|
+
file_path: null
|
|
1119
1286
|
});
|
|
1120
1287
|
}
|
|
1121
1288
|
|
|
1122
1289
|
// src/cli/capture.ts
|
|
1123
1290
|
var CAPTURE_HANG_TIMEOUT_MS = 5e3;
|
|
1124
1291
|
function registerCapture(program2) {
|
|
1125
|
-
program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).option("--git-before <sha>", "git HEAD SHA before this command ran").action((opts) => {
|
|
1292
|
+
program2.command("_capture").description("Internal: capture a shell command or IDE event (called by shell hook or IDE plugin)").option("--cmd <command>", "command that was executed (shell events)").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).option("--git-before <sha>", "git HEAD SHA before this command ran").option("--type <type>", "event type override \u2014 skips normalization (IDE integrations)").option("--source <source>", "event source identifier (e.g. vscode, neovim, jupyter)").option("--file <path>", "file path for code_edit events").action((opts) => {
|
|
1126
1293
|
const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
|
|
1127
1294
|
hangGuard.unref();
|
|
1128
1295
|
try {
|
|
1129
|
-
if (!opts.
|
|
1296
|
+
if (!opts.cwd) return;
|
|
1297
|
+
if (!opts.cmd && !opts.type) return;
|
|
1130
1298
|
captureCommand({
|
|
1131
1299
|
command: opts.cmd,
|
|
1132
1300
|
cwd: opts.cwd,
|
|
1133
1301
|
exitCode: opts.exit ?? null,
|
|
1134
1302
|
durationMs: opts.duration ?? null,
|
|
1135
|
-
gitBefore: opts.gitBefore ?? void 0
|
|
1303
|
+
gitBefore: opts.gitBefore ?? void 0,
|
|
1304
|
+
source: opts.source ?? void 0,
|
|
1305
|
+
type: opts.type ?? void 0,
|
|
1306
|
+
file: opts.file ?? void 0
|
|
1136
1307
|
});
|
|
1137
1308
|
} catch (err) {
|
|
1138
1309
|
debugLog("_capture", err);
|
|
@@ -1140,6 +1311,38 @@ function registerCapture(program2) {
|
|
|
1140
1311
|
});
|
|
1141
1312
|
}
|
|
1142
1313
|
|
|
1314
|
+
// src/capture/detect-secrets.ts
|
|
1315
|
+
function containsSecrets(text) {
|
|
1316
|
+
return redact(text) !== text;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/db/alerts.ts
|
|
1320
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1321
|
+
var import_path7 = __toESM(require("path"));
|
|
1322
|
+
function insertAlert(tool, sessionId, context) {
|
|
1323
|
+
getDb().prepare(
|
|
1324
|
+
`INSERT INTO alerts (created_at, tool, session_id, context)
|
|
1325
|
+
VALUES ($created_at, $tool, $session_id, $context)`
|
|
1326
|
+
).run({ created_at: Date.now(), tool, session_id: sessionId ?? null, context });
|
|
1327
|
+
import_fs11.default.mkdirSync(import_path7.default.dirname(ALERTS_PENDING_PATH), { recursive: true });
|
|
1328
|
+
import_fs11.default.writeFileSync(ALERTS_PENDING_PATH, "", { flag: "w" });
|
|
1329
|
+
}
|
|
1330
|
+
function getPendingAlerts() {
|
|
1331
|
+
return getDb().prepare("SELECT * FROM alerts WHERE cleared = 0 ORDER BY created_at DESC").all();
|
|
1332
|
+
}
|
|
1333
|
+
function hasPendingAlerts() {
|
|
1334
|
+
const row = getDb().prepare("SELECT COUNT(*) as count FROM alerts WHERE cleared = 0").get();
|
|
1335
|
+
return Number(row.count) > 0;
|
|
1336
|
+
}
|
|
1337
|
+
function clearAlerts() {
|
|
1338
|
+
const result = getDb().prepare("UPDATE alerts SET cleared = 1 WHERE cleared = 0").run();
|
|
1339
|
+
try {
|
|
1340
|
+
import_fs11.default.unlinkSync(ALERTS_PENDING_PATH);
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
return Number(result.changes);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1143
1346
|
// src/cli/hook.ts
|
|
1144
1347
|
function parseHookPayload(raw) {
|
|
1145
1348
|
if (!raw.trim()) return null;
|
|
@@ -1150,8 +1353,17 @@ function parseHookPayload(raw) {
|
|
|
1150
1353
|
if (!command || !cwd) return null;
|
|
1151
1354
|
return { command, cwd };
|
|
1152
1355
|
}
|
|
1356
|
+
function parseUserPromptPayload(raw) {
|
|
1357
|
+
if (!raw.trim()) return null;
|
|
1358
|
+
const payload = JSON.parse(raw);
|
|
1359
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : null;
|
|
1360
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
1361
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
|
|
1362
|
+
if (!prompt) return null;
|
|
1363
|
+
return { session_id: sessionId, prompt, cwd };
|
|
1364
|
+
}
|
|
1153
1365
|
function registerHook(program2) {
|
|
1154
|
-
program2.command("_hook").description("Internal: capture events from AI tool hooks (stdin JSON payload)").option("--source <source>", "hook source identifier (e.g. claude-code)").action((opts) => {
|
|
1366
|
+
program2.command("_hook").description("Internal: capture events from AI tool hooks (stdin JSON payload)").option("--source <source>", "hook source identifier (e.g. claude-code)").option("--event <event>", "hook event type (PostToolUse or UserPromptSubmit)", "PostToolUse").action((opts) => {
|
|
1155
1367
|
let raw = "";
|
|
1156
1368
|
process.stdin.setEncoding("utf-8");
|
|
1157
1369
|
process.stdin.on("data", (chunk) => {
|
|
@@ -1159,6 +1371,23 @@ function registerHook(program2) {
|
|
|
1159
1371
|
});
|
|
1160
1372
|
process.stdin.on("end", () => {
|
|
1161
1373
|
try {
|
|
1374
|
+
if (opts.event === "UserPromptSubmit") {
|
|
1375
|
+
const parsed2 = parseUserPromptPayload(raw);
|
|
1376
|
+
if (!parsed2) return;
|
|
1377
|
+
if (containsSecrets(parsed2.prompt)) {
|
|
1378
|
+
insertAlert(
|
|
1379
|
+
opts.source ?? "unknown",
|
|
1380
|
+
parsed2.session_id || null,
|
|
1381
|
+
"Possible credential detected in prompt submitted to AI tool"
|
|
1382
|
+
);
|
|
1383
|
+
process.stdout.write(
|
|
1384
|
+
JSON.stringify({
|
|
1385
|
+
systemMessage: "Workscribe detected a possible credential in your prompt. Run `workscribe alerts` for details."
|
|
1386
|
+
})
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1162
1391
|
if (readConfig().capture.aiTools === false) return;
|
|
1163
1392
|
const parsed = parseHookPayload(raw);
|
|
1164
1393
|
if (!parsed) return;
|
|
@@ -1177,8 +1406,8 @@ function registerHook(program2) {
|
|
|
1177
1406
|
}
|
|
1178
1407
|
|
|
1179
1408
|
// src/cli/init.ts
|
|
1180
|
-
var
|
|
1181
|
-
var
|
|
1409
|
+
var import_fs14 = __toESM(require("fs"));
|
|
1410
|
+
var import_path10 = __toESM(require("path"));
|
|
1182
1411
|
var import_os5 = __toESM(require("os"));
|
|
1183
1412
|
var import_prompts = __toESM(require("prompts"));
|
|
1184
1413
|
var import_chalk2 = __toESM(require("chalk"));
|
|
@@ -1224,6 +1453,17 @@ function eachDayOfRange(start, end) {
|
|
|
1224
1453
|
}
|
|
1225
1454
|
return days;
|
|
1226
1455
|
}
|
|
1456
|
+
function timeAgo(isoString, now = Date.now()) {
|
|
1457
|
+
const elapsedMs = now - new Date(isoString).getTime();
|
|
1458
|
+
const minutes = Math.floor(elapsedMs / 6e4);
|
|
1459
|
+
if (minutes < 2) return "just now";
|
|
1460
|
+
if (minutes < 60) return `${minutes} minutes ago`;
|
|
1461
|
+
const hours = Math.floor(minutes / 60);
|
|
1462
|
+
if (hours < 24) return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
1463
|
+
const days = Math.floor(hours / 24);
|
|
1464
|
+
if (days === 1) return "yesterday";
|
|
1465
|
+
return `${days} days ago`;
|
|
1466
|
+
}
|
|
1227
1467
|
function isoWeekKey(dateStr) {
|
|
1228
1468
|
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1229
1469
|
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
@@ -1272,6 +1512,8 @@ RULES \u2014 follow these exactly:
|
|
|
1272
1512
|
|
|
1273
1513
|
7. Stick to what the events show. Do not infer subject matter from the branch name. Do not add details not derivable from the events.
|
|
1274
1514
|
|
|
1515
|
+
8. For code_edit events: these represent file saves from an IDE integration. Describe what was being built based on repository context (branch name, commit messages). Never list individual filenames.
|
|
1516
|
+
|
|
1275
1517
|
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1276
1518
|
function getSystemPrompt() {
|
|
1277
1519
|
const contexts = getActiveProfileAiContexts();
|
|
@@ -1313,7 +1555,9 @@ function buildUserMessage(payload, includeCategories = []) {
|
|
|
1313
1555
|
if (excluded.has(event.category)) continue;
|
|
1314
1556
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1315
1557
|
let key;
|
|
1316
|
-
if (event.
|
|
1558
|
+
if (event.category === "code_edit") {
|
|
1559
|
+
key = `code_edit${failed}`;
|
|
1560
|
+
} else if (event.hint) {
|
|
1317
1561
|
key = `other [${event.hint}]${failed}`;
|
|
1318
1562
|
} else {
|
|
1319
1563
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
@@ -1381,7 +1625,9 @@ function buildWeeklyMessage(payload) {
|
|
|
1381
1625
|
if (excluded.has(event.category)) continue;
|
|
1382
1626
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1383
1627
|
let key;
|
|
1384
|
-
if (event.
|
|
1628
|
+
if (event.category === "code_edit") {
|
|
1629
|
+
key = `code_edit${failed}`;
|
|
1630
|
+
} else if (event.hint) {
|
|
1385
1631
|
key = `other [${event.hint}]${failed}`;
|
|
1386
1632
|
} else {
|
|
1387
1633
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
@@ -1412,7 +1658,7 @@ var GenericProvider = class {
|
|
|
1412
1658
|
baseUrl;
|
|
1413
1659
|
includeCategories;
|
|
1414
1660
|
async call(system, userMsg, maxTokens) {
|
|
1415
|
-
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
|
|
1661
|
+
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl, this.apiKey, this.model), {
|
|
1416
1662
|
method: "POST",
|
|
1417
1663
|
headers: this.config.headers(this.apiKey),
|
|
1418
1664
|
body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
|
|
@@ -1430,7 +1676,7 @@ var GenericProvider = class {
|
|
|
1430
1676
|
}
|
|
1431
1677
|
async validate() {
|
|
1432
1678
|
const isPost = this.config.validateMethod === "POST";
|
|
1433
|
-
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
|
|
1679
|
+
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl, this.apiKey, this.model), {
|
|
1434
1680
|
method: this.config.validateMethod,
|
|
1435
1681
|
headers: isPost ? this.config.headers(this.apiKey) : void 0,
|
|
1436
1682
|
body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
|
|
@@ -1529,6 +1775,59 @@ var PROVIDER_CONFIGS = {
|
|
|
1529
1775
|
},
|
|
1530
1776
|
chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
|
|
1531
1777
|
validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
|
|
1778
|
+
},
|
|
1779
|
+
gemini: {
|
|
1780
|
+
defaultModel: "gemini-2.0-flash-lite",
|
|
1781
|
+
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
1782
|
+
dailyTokens: 1024,
|
|
1783
|
+
weeklyTokens: 1024,
|
|
1784
|
+
// API key and model are embedded in the URL — not in headers
|
|
1785
|
+
chatUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
|
|
1786
|
+
validateUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
|
|
1787
|
+
validateMethod: "POST",
|
|
1788
|
+
headers: () => ({ "Content-Type": "application/json" }),
|
|
1789
|
+
buildChatBody: (_model, system, userMsg, maxTokens) => ({
|
|
1790
|
+
system_instruction: { parts: [{ text: system }] },
|
|
1791
|
+
contents: [{ role: "user", parts: [{ text: userMsg }] }],
|
|
1792
|
+
generationConfig: { maxOutputTokens: maxTokens, temperature: 0.3 }
|
|
1793
|
+
}),
|
|
1794
|
+
buildValidateBody: () => ({
|
|
1795
|
+
contents: [{ role: "user", parts: [{ text: "." }] }],
|
|
1796
|
+
generationConfig: { maxOutputTokens: 1 }
|
|
1797
|
+
}),
|
|
1798
|
+
parseText: (json) => {
|
|
1799
|
+
const data = json;
|
|
1800
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
1801
|
+
},
|
|
1802
|
+
chatError: (status, body) => `Gemini API error ${status}: ${body}`,
|
|
1803
|
+
validateError: (status, body) => `Gemini API error ${status}: ${body}`
|
|
1804
|
+
},
|
|
1805
|
+
groq: {
|
|
1806
|
+
defaultModel: "llama-3.1-8b-instant",
|
|
1807
|
+
defaultBaseUrl: "https://api.groq.com/openai/v1",
|
|
1808
|
+
dailyTokens: 1024,
|
|
1809
|
+
weeklyTokens: 1024,
|
|
1810
|
+
chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
1811
|
+
validateUrl: (baseUrl) => `${baseUrl}/models`,
|
|
1812
|
+
validateMethod: "GET",
|
|
1813
|
+
headers: (apiKey) => ({
|
|
1814
|
+
"Content-Type": "application/json",
|
|
1815
|
+
Authorization: `Bearer ${apiKey}`
|
|
1816
|
+
}),
|
|
1817
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1818
|
+
model,
|
|
1819
|
+
messages: [
|
|
1820
|
+
{ role: "system", content: system },
|
|
1821
|
+
{ role: "user", content: userMsg }
|
|
1822
|
+
],
|
|
1823
|
+
max_tokens: maxTokens
|
|
1824
|
+
}),
|
|
1825
|
+
parseText: (json) => {
|
|
1826
|
+
const data = json;
|
|
1827
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
1828
|
+
},
|
|
1829
|
+
chatError: (status, body) => `Groq API error ${status}: ${body}`,
|
|
1830
|
+
validateError: (status, body) => `Groq API error ${status}: ${body}`
|
|
1532
1831
|
}
|
|
1533
1832
|
};
|
|
1534
1833
|
|
|
@@ -1537,7 +1836,7 @@ function getProvider(config) {
|
|
|
1537
1836
|
const { provider, apiKey = "", model, baseUrl } = config.ai;
|
|
1538
1837
|
if (!provider) {
|
|
1539
1838
|
throw new Error(
|
|
1540
|
-
"No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
|
|
1839
|
+
"No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|gemini|groq|ollama|openai-compatible>"
|
|
1541
1840
|
);
|
|
1542
1841
|
}
|
|
1543
1842
|
const configKey = provider === "openai-compatible" ? "openai" : provider;
|
|
@@ -1545,7 +1844,7 @@ function getProvider(config) {
|
|
|
1545
1844
|
if (!providerConfig) {
|
|
1546
1845
|
throw new Error(
|
|
1547
1846
|
`Unknown provider: "${provider}"
|
|
1548
|
-
Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
1847
|
+
Supported providers: anthropic, openai, gemini, groq, ollama, openai-compatible`
|
|
1549
1848
|
);
|
|
1550
1849
|
}
|
|
1551
1850
|
if (provider !== "ollama" && !apiKey) {
|
|
@@ -1565,9 +1864,10 @@ Run: workscribe config set ai.apiKey <your-key>`);
|
|
|
1565
1864
|
}
|
|
1566
1865
|
|
|
1567
1866
|
// src/capture/shell-hook.ts
|
|
1568
|
-
var
|
|
1569
|
-
var
|
|
1867
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1868
|
+
var import_path8 = __toESM(require("path"));
|
|
1570
1869
|
var import_os3 = __toESM(require("os"));
|
|
1870
|
+
var import_child_process3 = require("child_process");
|
|
1571
1871
|
|
|
1572
1872
|
// src/utils/completion-scripts.ts
|
|
1573
1873
|
function bashCompletion(commands) {
|
|
@@ -1624,13 +1924,13 @@ function fishCompletion(commands) {
|
|
|
1624
1924
|
}
|
|
1625
1925
|
|
|
1626
1926
|
// src/capture/shell-hook.ts
|
|
1627
|
-
var FISH_COMPLETIONS_DIR =
|
|
1927
|
+
var FISH_COMPLETIONS_DIR = import_path8.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
|
|
1628
1928
|
var HOOK_START = "# workscribe:hook:start";
|
|
1629
1929
|
var HOOK_END = "# workscribe:hook:end";
|
|
1630
1930
|
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
1631
1931
|
function getInstalledHookBlock(rcFile) {
|
|
1632
|
-
if (!
|
|
1633
|
-
const content =
|
|
1932
|
+
if (!import_fs12.default.existsSync(rcFile)) return null;
|
|
1933
|
+
const content = import_fs12.default.readFileSync(rcFile, "utf-8");
|
|
1634
1934
|
const startIdx = content.indexOf(HOOK_START);
|
|
1635
1935
|
const endIdx = content.indexOf(HOOK_END);
|
|
1636
1936
|
if (startIdx === -1 || endIdx === -1) return null;
|
|
@@ -1646,54 +1946,66 @@ function isHookOutdated(rcFile, hookContent) {
|
|
|
1646
1946
|
}
|
|
1647
1947
|
function installHook(rcFile, hookContent) {
|
|
1648
1948
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1649
|
-
|
|
1650
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1651
|
-
|
|
1949
|
+
import_fs12.default.copyFileSync(rcFile, backup);
|
|
1950
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
|
|
1951
|
+
import_fs12.default.appendFileSync(rcFile, `
|
|
1652
1952
|
${hookContent}
|
|
1653
1953
|
`);
|
|
1654
1954
|
}
|
|
1655
1955
|
function updateHook(rcFile, hookContent) {
|
|
1656
1956
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1657
|
-
|
|
1658
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1659
|
-
const content =
|
|
1957
|
+
import_fs12.default.copyFileSync(rcFile, backup);
|
|
1958
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
|
|
1959
|
+
const content = import_fs12.default.readFileSync(rcFile, "utf-8");
|
|
1660
1960
|
const startIdx = content.indexOf(HOOK_START);
|
|
1661
1961
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1662
|
-
|
|
1962
|
+
import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
1663
1963
|
}
|
|
1664
1964
|
function removeHook(rcFile) {
|
|
1665
1965
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1666
|
-
|
|
1667
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1668
|
-
const content =
|
|
1966
|
+
import_fs12.default.copyFileSync(rcFile, backup);
|
|
1967
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
|
|
1968
|
+
const content = import_fs12.default.readFileSync(rcFile, "utf-8");
|
|
1669
1969
|
const startIdx = content.indexOf(HOOK_START);
|
|
1670
1970
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1671
1971
|
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
1672
1972
|
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
1673
1973
|
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
1674
1974
|
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
1675
|
-
|
|
1975
|
+
import_fs12.default.writeFileSync(rcFile, before + after);
|
|
1976
|
+
}
|
|
1977
|
+
function resolveWorkscribeBinary() {
|
|
1978
|
+
try {
|
|
1979
|
+
const resolved = (0, import_child_process3.execFileSync)("which", ["workscribe"], { encoding: "utf-8" }).trim();
|
|
1980
|
+
return resolved || null;
|
|
1981
|
+
} catch {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function injectBinaryPath(hookContent, binaryPath) {
|
|
1986
|
+
return hookContent.replace("command -v workscribe > /dev/null 2>&1 || return 0", `[[ -x "${binaryPath}" ]] || return 0`).replace("command -v workscribe &>/dev/null || return 0", `[[ -x "${binaryPath}" ]] || return 0`).replace("type -q workscribe\n or return 0", `test -x "${binaryPath}"
|
|
1987
|
+
or return 0`).replaceAll("command workscribe _capture", `"${binaryPath}" _capture`).replaceAll("command workscribe alerts", `"${binaryPath}" alerts`);
|
|
1676
1988
|
}
|
|
1677
1989
|
function isCompletionsInstalled(shell) {
|
|
1678
1990
|
if (shell.name === "fish") {
|
|
1679
|
-
return
|
|
1991
|
+
return import_fs12.default.existsSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
|
|
1680
1992
|
}
|
|
1681
1993
|
try {
|
|
1682
|
-
return
|
|
1994
|
+
return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
|
|
1683
1995
|
} catch {
|
|
1684
1996
|
return false;
|
|
1685
1997
|
}
|
|
1686
1998
|
}
|
|
1687
1999
|
function installCompletions(shell, commands) {
|
|
1688
2000
|
if (shell.name === "fish") {
|
|
1689
|
-
if (!
|
|
1690
|
-
|
|
2001
|
+
if (!import_fs12.default.existsSync(FISH_COMPLETIONS_DIR)) {
|
|
2002
|
+
import_fs12.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
|
|
1691
2003
|
}
|
|
1692
|
-
|
|
2004
|
+
import_fs12.default.writeFileSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
|
|
1693
2005
|
} else {
|
|
1694
2006
|
const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
|
|
1695
|
-
const cacheFile =
|
|
1696
|
-
|
|
2007
|
+
const cacheFile = import_path8.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
|
|
2008
|
+
import_fs12.default.writeFileSync(cacheFile, script, { mode: 420 });
|
|
1697
2009
|
const sourceLine = shell.name === "zsh" ? `
|
|
1698
2010
|
# Workscribe completions
|
|
1699
2011
|
autoload -Uz compinit && compinit
|
|
@@ -1702,20 +2014,20 @@ source "${cacheFile}"
|
|
|
1702
2014
|
# Workscribe completions
|
|
1703
2015
|
source "${cacheFile}"
|
|
1704
2016
|
`;
|
|
1705
|
-
|
|
2017
|
+
import_fs12.default.appendFileSync(shell.rcFile, sourceLine);
|
|
1706
2018
|
}
|
|
1707
2019
|
}
|
|
1708
2020
|
|
|
1709
2021
|
// src/capture/hooks/claude-code.ts
|
|
1710
|
-
var
|
|
1711
|
-
var
|
|
2022
|
+
var import_fs13 = __toESM(require("fs"));
|
|
2023
|
+
var import_path9 = __toESM(require("path"));
|
|
1712
2024
|
var import_os4 = __toESM(require("os"));
|
|
1713
2025
|
|
|
1714
2026
|
// src/capture/hooks/utils.ts
|
|
1715
|
-
var
|
|
2027
|
+
var import_child_process4 = require("child_process");
|
|
1716
2028
|
function isCommandInstalled(cmd) {
|
|
1717
2029
|
try {
|
|
1718
|
-
(0,
|
|
2030
|
+
(0, import_child_process4.execSync)(`which ${cmd}`, { stdio: "ignore" });
|
|
1719
2031
|
return true;
|
|
1720
2032
|
} catch {
|
|
1721
2033
|
return false;
|
|
@@ -1723,11 +2035,11 @@ function isCommandInstalled(cmd) {
|
|
|
1723
2035
|
}
|
|
1724
2036
|
|
|
1725
2037
|
// src/capture/hooks/claude-code.ts
|
|
1726
|
-
var CLAUDE_SETTINGS_PATH =
|
|
2038
|
+
var CLAUDE_SETTINGS_PATH = import_path9.default.join(import_os4.default.homedir(), ".claude", "settings.json");
|
|
1727
2039
|
function readSettings() {
|
|
1728
|
-
if (!
|
|
2040
|
+
if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
1729
2041
|
try {
|
|
1730
|
-
return JSON.parse(
|
|
2042
|
+
return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
1731
2043
|
} catch {
|
|
1732
2044
|
return {};
|
|
1733
2045
|
}
|
|
@@ -1739,19 +2051,30 @@ var claudeCodeIntegration = {
|
|
|
1739
2051
|
},
|
|
1740
2052
|
isHookInstalled() {
|
|
1741
2053
|
const settings = readSettings();
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
2054
|
+
const hooks = settings.hooks ?? {};
|
|
2055
|
+
const postToolUse = hooks.PostToolUse ?? [];
|
|
2056
|
+
const hasPostToolUse = postToolUse.some((entry) => {
|
|
2057
|
+
const entryHooks = entry.hooks ?? [];
|
|
2058
|
+
return entryHooks.some((hookItem) => {
|
|
2059
|
+
const hook = hookItem;
|
|
2060
|
+
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
2061
|
+
});
|
|
2062
|
+
});
|
|
2063
|
+
const userPromptSubmit = hooks.UserPromptSubmit ?? [];
|
|
2064
|
+
const hasUserPromptSubmit = userPromptSubmit.some((entry) => {
|
|
2065
|
+
const entryHooks = entry.hooks ?? [];
|
|
2066
|
+
return entryHooks.some((hookItem) => {
|
|
1746
2067
|
const hook = hookItem;
|
|
1747
2068
|
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
1748
2069
|
});
|
|
1749
2070
|
});
|
|
2071
|
+
return hasPostToolUse && hasUserPromptSubmit;
|
|
1750
2072
|
},
|
|
1751
2073
|
install() {
|
|
1752
2074
|
const settings = readSettings();
|
|
1753
2075
|
const existingHooks = settings.hooks ?? {};
|
|
1754
2076
|
const existingPostToolUse = existingHooks.PostToolUse ?? [];
|
|
2077
|
+
const existingUserPromptSubmit = existingHooks.UserPromptSubmit ?? [];
|
|
1755
2078
|
settings.hooks = {
|
|
1756
2079
|
...existingHooks,
|
|
1757
2080
|
PostToolUse: [
|
|
@@ -1760,10 +2083,21 @@ var claudeCodeIntegration = {
|
|
|
1760
2083
|
matcher: "Bash",
|
|
1761
2084
|
hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
|
|
1762
2085
|
}
|
|
2086
|
+
],
|
|
2087
|
+
UserPromptSubmit: [
|
|
2088
|
+
...existingUserPromptSubmit,
|
|
2089
|
+
{
|
|
2090
|
+
hooks: [
|
|
2091
|
+
{
|
|
2092
|
+
type: "command",
|
|
2093
|
+
command: "workscribe _hook --source claude-code --event UserPromptSubmit"
|
|
2094
|
+
}
|
|
2095
|
+
]
|
|
2096
|
+
}
|
|
1763
2097
|
]
|
|
1764
2098
|
};
|
|
1765
|
-
|
|
1766
|
-
|
|
2099
|
+
import_fs13.default.mkdirSync(import_path9.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
2100
|
+
import_fs13.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
1767
2101
|
}
|
|
1768
2102
|
};
|
|
1769
2103
|
|
|
@@ -1794,11 +2128,11 @@ function registerCompletions(program2) {
|
|
|
1794
2128
|
// src/cli/init.ts
|
|
1795
2129
|
function detectShell() {
|
|
1796
2130
|
const shell = process.env.SHELL ?? "";
|
|
1797
|
-
if (shell.includes("zsh")) return { name: "zsh", rcFile:
|
|
1798
|
-
if (shell.includes("bash")) return { name: "bash", rcFile:
|
|
2131
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path10.default.join(import_os5.default.homedir(), ".zshrc") };
|
|
2132
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path10.default.join(import_os5.default.homedir(), ".bashrc") };
|
|
1799
2133
|
if (shell.includes("fish")) {
|
|
1800
|
-
const confDir =
|
|
1801
|
-
return { name: "fish", rcFile:
|
|
2134
|
+
const confDir = import_path10.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
|
|
2135
|
+
return { name: "fish", rcFile: import_path10.default.join(confDir, "workscribe.fish") };
|
|
1802
2136
|
}
|
|
1803
2137
|
return null;
|
|
1804
2138
|
}
|
|
@@ -1812,19 +2146,19 @@ function registerInit(program2) {
|
|
|
1812
2146
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1813
2147
|
step(1, TOTAL_STEPS, "Creating data directories");
|
|
1814
2148
|
for (const dir of REQUIRED_DIRS) {
|
|
1815
|
-
if (!
|
|
1816
|
-
|
|
2149
|
+
if (!import_fs14.default.existsSync(dir)) {
|
|
2150
|
+
import_fs14.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1817
2151
|
log.success(`Created ${dir}`);
|
|
1818
2152
|
} else {
|
|
1819
2153
|
try {
|
|
1820
|
-
|
|
2154
|
+
import_fs14.default.chmodSync(dir, 448);
|
|
1821
2155
|
} catch {
|
|
1822
2156
|
}
|
|
1823
2157
|
log.info(`${dir} already exists`);
|
|
1824
2158
|
}
|
|
1825
2159
|
}
|
|
1826
|
-
if (!
|
|
1827
|
-
|
|
2160
|
+
if (!import_fs14.default.existsSync(PROFILES_DIR)) {
|
|
2161
|
+
import_fs14.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1828
2162
|
}
|
|
1829
2163
|
step(2, TOTAL_STEPS, "Initialising database");
|
|
1830
2164
|
getDb();
|
|
@@ -1837,16 +2171,18 @@ function registerInit(program2) {
|
|
|
1837
2171
|
` Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")}, ${import_chalk2.default.cyan("shell/hook.bash")}, or ${import_chalk2.default.cyan("shell/hook.fish")}`
|
|
1838
2172
|
);
|
|
1839
2173
|
} else {
|
|
1840
|
-
const hookFile =
|
|
1841
|
-
if (!
|
|
2174
|
+
const hookFile = import_path10.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
2175
|
+
if (!import_fs14.default.existsSync(hookFile)) {
|
|
1842
2176
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
1843
2177
|
} else {
|
|
1844
|
-
const hookContent =
|
|
2178
|
+
const hookContent = import_fs14.default.readFileSync(hookFile, "utf-8");
|
|
2179
|
+
const binaryPath = resolveWorkscribeBinary();
|
|
2180
|
+
const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath) : hookContent;
|
|
1845
2181
|
if (shell.name === "fish") {
|
|
1846
|
-
const confDir =
|
|
1847
|
-
if (!
|
|
2182
|
+
const confDir = import_path10.default.dirname(shell.rcFile);
|
|
2183
|
+
if (!import_fs14.default.existsSync(confDir)) import_fs14.default.mkdirSync(confDir, { recursive: true });
|
|
1848
2184
|
}
|
|
1849
|
-
if (!
|
|
2185
|
+
if (!import_fs14.default.existsSync(shell.rcFile)) import_fs14.default.writeFileSync(shell.rcFile, "");
|
|
1850
2186
|
if (!isHookInstalled(shell.rcFile)) {
|
|
1851
2187
|
log.plain("");
|
|
1852
2188
|
log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
|
|
@@ -1854,9 +2190,9 @@ function registerInit(program2) {
|
|
|
1854
2190
|
log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
|
|
1855
2191
|
log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
|
|
1856
2192
|
log.plain("");
|
|
1857
|
-
installHook(shell.rcFile,
|
|
2193
|
+
installHook(shell.rcFile, hookWithPath);
|
|
1858
2194
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
1859
|
-
} else if (isHookOutdated(shell.rcFile,
|
|
2195
|
+
} else if (isHookOutdated(shell.rcFile, hookWithPath)) {
|
|
1860
2196
|
const { update } = await (0, import_prompts.default)({
|
|
1861
2197
|
type: "confirm",
|
|
1862
2198
|
name: "update",
|
|
@@ -1864,7 +2200,7 @@ function registerInit(program2) {
|
|
|
1864
2200
|
initial: true
|
|
1865
2201
|
});
|
|
1866
2202
|
if (update) {
|
|
1867
|
-
updateHook(shell.rcFile,
|
|
2203
|
+
updateHook(shell.rcFile, hookWithPath);
|
|
1868
2204
|
log.success(`Shell hook updated in ${shell.rcFile}`);
|
|
1869
2205
|
} else {
|
|
1870
2206
|
log.info("Shell hook update skipped");
|
|
@@ -2020,7 +2356,7 @@ function registerInit(program2) {
|
|
|
2020
2356
|
}
|
|
2021
2357
|
}
|
|
2022
2358
|
}
|
|
2023
|
-
if (!
|
|
2359
|
+
if (!import_fs14.default.existsSync(CONFIG_PATH)) {
|
|
2024
2360
|
writeConfig(config);
|
|
2025
2361
|
}
|
|
2026
2362
|
log.plain("");
|
|
@@ -2056,7 +2392,7 @@ function registerInit(program2) {
|
|
|
2056
2392
|
}
|
|
2057
2393
|
|
|
2058
2394
|
// src/cli/status.ts
|
|
2059
|
-
var
|
|
2395
|
+
var import_fs15 = __toESM(require("fs"));
|
|
2060
2396
|
var import_chalk3 = __toESM(require("chalk"));
|
|
2061
2397
|
function registerStatus(program2) {
|
|
2062
2398
|
program2.command("status").description("Show capture status and event counts").action(() => {
|
|
@@ -2064,7 +2400,7 @@ function registerStatus(program2) {
|
|
|
2064
2400
|
const total = getEventCount();
|
|
2065
2401
|
const todayEvents = getEventsForDate(todayDate());
|
|
2066
2402
|
const last = getLastEvent();
|
|
2067
|
-
const isPaused =
|
|
2403
|
+
const isPaused = import_fs15.default.existsSync(PAUSE_FLAG_PATH);
|
|
2068
2404
|
log.plain("");
|
|
2069
2405
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
2070
2406
|
log.plain("");
|
|
@@ -2087,6 +2423,13 @@ function registerStatus(program2) {
|
|
|
2087
2423
|
log.plain(
|
|
2088
2424
|
` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
|
|
2089
2425
|
);
|
|
2426
|
+
const pendingAlerts = getPendingAlerts();
|
|
2427
|
+
if (pendingAlerts.length > 0) {
|
|
2428
|
+
log.plain("");
|
|
2429
|
+
log.plain(
|
|
2430
|
+
` ${import_chalk3.default.yellow("!")} ${import_chalk3.default.yellow(String(pendingAlerts.length))} pending security alert${pendingAlerts.length === 1 ? "" : "s"}` + import_chalk3.default.dim(" (run: workscribe alerts)")
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2090
2433
|
const { total: weekTotal, otherCount } = getRecentOtherRate(7);
|
|
2091
2434
|
if (weekTotal > 0) {
|
|
2092
2435
|
const otherPct = Math.round(otherCount / weekTotal * 100);
|
|
@@ -2180,13 +2523,16 @@ function registerEvents(program2) {
|
|
|
2180
2523
|
var import_chalk5 = __toESM(require("chalk"));
|
|
2181
2524
|
|
|
2182
2525
|
// src/sessions/grouper.ts
|
|
2526
|
+
var LONG_RUNNING_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2183
2527
|
function splitByIdleGap(events, idleMs) {
|
|
2184
2528
|
if (events.length === 0) return [];
|
|
2185
2529
|
const groups = [[events[0]]];
|
|
2186
2530
|
for (let i = 1; i < events.length; i++) {
|
|
2187
2531
|
const prev = new Date(events[i - 1].captured_at).getTime();
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2532
|
+
const currCompleted = new Date(events[i].captured_at).getTime();
|
|
2533
|
+
const currDuration = events[i].duration_ms ?? 0;
|
|
2534
|
+
const currEffective = currDuration >= LONG_RUNNING_THRESHOLD_MS ? currCompleted - currDuration : currCompleted;
|
|
2535
|
+
if (currEffective - prev >= idleMs) {
|
|
2190
2536
|
groups.push([events[i]]);
|
|
2191
2537
|
} else {
|
|
2192
2538
|
groups[groups.length - 1].push(events[i]);
|
|
@@ -2194,13 +2540,17 @@ function splitByIdleGap(events, idleMs) {
|
|
|
2194
2540
|
}
|
|
2195
2541
|
return groups;
|
|
2196
2542
|
}
|
|
2197
|
-
|
|
2543
|
+
var REPO_SWITCH_GAP_MS = 10 * 60 * 1e3;
|
|
2544
|
+
function splitByRepo(events, gapMs = 0) {
|
|
2198
2545
|
if (events.length === 0) return [];
|
|
2199
2546
|
const groups = [[events[0]]];
|
|
2200
2547
|
for (let i = 1; i < events.length; i++) {
|
|
2201
2548
|
const prevRepo = events[i - 1].repo_name;
|
|
2202
2549
|
const currRepo = events[i].repo_name;
|
|
2203
|
-
|
|
2550
|
+
const prevTime = new Date(events[i - 1].captured_at).getTime();
|
|
2551
|
+
const currTime = new Date(events[i].captured_at).getTime();
|
|
2552
|
+
const gap = currTime - prevTime;
|
|
2553
|
+
if (currRepo !== prevRepo && gap >= gapMs) {
|
|
2204
2554
|
groups.push([events[i]]);
|
|
2205
2555
|
} else {
|
|
2206
2556
|
groups[groups.length - 1].push(events[i]);
|
|
@@ -2245,7 +2595,7 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
|
|
|
2245
2595
|
const timeGroups = splitByIdleGap(events, idleMs);
|
|
2246
2596
|
for (const group of timeGroups) {
|
|
2247
2597
|
if (group.length === 0) continue;
|
|
2248
|
-
const repoGroups = splitByRepo(group);
|
|
2598
|
+
const repoGroups = splitByRepo(group, REPO_SWITCH_GAP_MS);
|
|
2249
2599
|
for (const repoGroup of repoGroups) {
|
|
2250
2600
|
if (repoGroup.length === 0) continue;
|
|
2251
2601
|
const first = repoGroup[0];
|
|
@@ -2324,9 +2674,9 @@ function registerSessions(program2) {
|
|
|
2324
2674
|
|
|
2325
2675
|
// src/cli/summary.ts
|
|
2326
2676
|
var import_chalk6 = __toESM(require("chalk"));
|
|
2327
|
-
var
|
|
2677
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2328
2678
|
var import_os6 = __toESM(require("os"));
|
|
2329
|
-
var
|
|
2679
|
+
var import_path11 = __toESM(require("path"));
|
|
2330
2680
|
|
|
2331
2681
|
// src/integrations/webhook.ts
|
|
2332
2682
|
async function postToWebhook(url, text) {
|
|
@@ -2431,9 +2781,9 @@ function registerSummary(program2) {
|
|
|
2431
2781
|
return;
|
|
2432
2782
|
}
|
|
2433
2783
|
const cacheKey = isoWeekKey(today);
|
|
2434
|
-
const cacheFile =
|
|
2435
|
-
if (!opts.fresh &&
|
|
2436
|
-
const cached =
|
|
2784
|
+
const cacheFile = import_path11.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
2785
|
+
if (!opts.fresh && import_fs16.default.existsSync(cacheFile)) {
|
|
2786
|
+
const cached = import_fs16.default.readFileSync(cacheFile, "utf-8");
|
|
2437
2787
|
log.plain("");
|
|
2438
2788
|
log.plain(cached);
|
|
2439
2789
|
log.plain("");
|
|
@@ -2449,8 +2799,8 @@ function registerSummary(program2) {
|
|
|
2449
2799
|
log.plain("");
|
|
2450
2800
|
log.plain(summary);
|
|
2451
2801
|
log.plain("");
|
|
2452
|
-
|
|
2453
|
-
|
|
2802
|
+
import_fs16.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2803
|
+
import_fs16.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
2454
2804
|
await handleWebhook(opts, config, summary);
|
|
2455
2805
|
} catch (error) {
|
|
2456
2806
|
log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
|
|
@@ -2568,8 +2918,8 @@ ${bullets.trim()}`;
|
|
|
2568
2918
|
}
|
|
2569
2919
|
|
|
2570
2920
|
// src/cli/export.ts
|
|
2571
|
-
var
|
|
2572
|
-
var
|
|
2921
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2922
|
+
var import_path12 = __toESM(require("path"));
|
|
2573
2923
|
function eventRowToPayload2(row) {
|
|
2574
2924
|
let metadata = {};
|
|
2575
2925
|
try {
|
|
@@ -2625,9 +2975,9 @@ function registerExport(program2) {
|
|
|
2625
2975
|
const payload = { date, sessions: sessionPayloads };
|
|
2626
2976
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2627
2977
|
const summary = await provider.summarize(payload);
|
|
2628
|
-
const outputPath = opts.output ??
|
|
2629
|
-
if (!
|
|
2630
|
-
|
|
2978
|
+
const outputPath = opts.output ?? import_path12.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
2979
|
+
if (!import_fs17.default.existsSync(EXPORTS_DIR)) import_fs17.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
2980
|
+
import_fs17.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
2631
2981
|
|
|
2632
2982
|
${summary}
|
|
2633
2983
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2699,22 +3049,22 @@ function printConfig(obj, indent) {
|
|
|
2699
3049
|
}
|
|
2700
3050
|
|
|
2701
3051
|
// src/cli/pause.ts
|
|
2702
|
-
var
|
|
3052
|
+
var import_fs18 = __toESM(require("fs"));
|
|
2703
3053
|
function registerPause(program2) {
|
|
2704
3054
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2705
|
-
if (
|
|
3055
|
+
if (import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2706
3056
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2707
3057
|
return;
|
|
2708
3058
|
}
|
|
2709
|
-
|
|
3059
|
+
import_fs18.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2710
3060
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2711
3061
|
});
|
|
2712
3062
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2713
|
-
if (!
|
|
3063
|
+
if (!import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2714
3064
|
log.info("Capture is not paused.");
|
|
2715
3065
|
return;
|
|
2716
3066
|
}
|
|
2717
|
-
|
|
3067
|
+
import_fs18.default.rmSync(PAUSE_FLAG_PATH);
|
|
2718
3068
|
log.success("Capture resumed.");
|
|
2719
3069
|
});
|
|
2720
3070
|
}
|
|
@@ -2799,19 +3149,19 @@ function registerClear(program2) {
|
|
|
2799
3149
|
}
|
|
2800
3150
|
|
|
2801
3151
|
// src/cli/uninstall.ts
|
|
2802
|
-
var
|
|
2803
|
-
var
|
|
3152
|
+
var import_fs19 = __toESM(require("fs"));
|
|
3153
|
+
var import_path13 = __toESM(require("path"));
|
|
2804
3154
|
var import_os7 = __toESM(require("os"));
|
|
2805
3155
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2806
3156
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2807
3157
|
function detectShellRcFiles() {
|
|
2808
3158
|
const candidates = [
|
|
2809
|
-
{ name: "zsh", rcFile:
|
|
2810
|
-
{ name: "bash", rcFile:
|
|
2811
|
-
{ name: "bash", rcFile:
|
|
2812
|
-
{ name: "fish", rcFile:
|
|
3159
|
+
{ name: "zsh", rcFile: import_path13.default.join(import_os7.default.homedir(), ".zshrc") },
|
|
3160
|
+
{ name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bashrc") },
|
|
3161
|
+
{ name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bash_profile") },
|
|
3162
|
+
{ name: "fish", rcFile: import_path13.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
|
|
2813
3163
|
];
|
|
2814
|
-
return candidates.filter((candidate) =>
|
|
3164
|
+
return candidates.filter((candidate) => import_fs19.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2815
3165
|
}
|
|
2816
3166
|
function registerUninstall(program2) {
|
|
2817
3167
|
program2.command("uninstall").description("Remove shell hooks and optionally delete all Workscribe data").option("--hooks-only", "remove shell hooks only, skip data deletion prompt (used by npm preuninstall)").action(async (opts) => {
|
|
@@ -2822,7 +3172,7 @@ function registerUninstall(program2) {
|
|
|
2822
3172
|
} else {
|
|
2823
3173
|
for (const { name, rcFile } of hooked) {
|
|
2824
3174
|
if (name === "fish") {
|
|
2825
|
-
|
|
3175
|
+
import_fs19.default.unlinkSync(rcFile);
|
|
2826
3176
|
log.success(`Shell hook removed: ${rcFile}`);
|
|
2827
3177
|
log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
|
|
2828
3178
|
} else {
|
|
@@ -2836,7 +3186,7 @@ function registerUninstall(program2) {
|
|
|
2836
3186
|
}
|
|
2837
3187
|
}
|
|
2838
3188
|
if (opts.hooksOnly) return;
|
|
2839
|
-
const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) =>
|
|
3189
|
+
const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs19.default.existsSync(dir));
|
|
2840
3190
|
if (existingDirs.length > 0) {
|
|
2841
3191
|
log.plain("");
|
|
2842
3192
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
@@ -2847,7 +3197,7 @@ function registerUninstall(program2) {
|
|
|
2847
3197
|
});
|
|
2848
3198
|
if (deleteData) {
|
|
2849
3199
|
for (const dir of existingDirs) {
|
|
2850
|
-
|
|
3200
|
+
import_fs19.default.rmSync(dir, { recursive: true, force: true });
|
|
2851
3201
|
log.success(`Deleted ${dir}`);
|
|
2852
3202
|
}
|
|
2853
3203
|
} else {
|
|
@@ -2861,8 +3211,8 @@ function registerUninstall(program2) {
|
|
|
2861
3211
|
}
|
|
2862
3212
|
|
|
2863
3213
|
// src/cli/profile.ts
|
|
2864
|
-
var
|
|
2865
|
-
var
|
|
3214
|
+
var import_fs20 = __toESM(require("fs"));
|
|
3215
|
+
var import_path14 = __toESM(require("path"));
|
|
2866
3216
|
var import_chalk10 = __toESM(require("chalk"));
|
|
2867
3217
|
var MIN_SUGGEST_COUNT = 5;
|
|
2868
3218
|
function registerProfile(program2) {
|
|
@@ -2908,7 +3258,7 @@ function registerProfile(program2) {
|
|
|
2908
3258
|
log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
|
|
2909
3259
|
for (const entry of entries) {
|
|
2910
3260
|
const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
|
|
2911
|
-
const installed =
|
|
3261
|
+
const installed = import_fs20.default.existsSync(import_path14.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
|
|
2912
3262
|
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
|
|
2913
3263
|
if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
|
|
2914
3264
|
}
|
|
@@ -2944,8 +3294,8 @@ function registerProfile(program2) {
|
|
|
2944
3294
|
continue;
|
|
2945
3295
|
}
|
|
2946
3296
|
activeNames.delete(name);
|
|
2947
|
-
const cachePath =
|
|
2948
|
-
if (
|
|
3297
|
+
const cachePath = import_path14.default.join(PROFILES_DIR, `${name}.json`);
|
|
3298
|
+
if (import_fs20.default.existsSync(cachePath)) import_fs20.default.unlinkSync(cachePath);
|
|
2949
3299
|
log.success(`Profile "${name}" removed`);
|
|
2950
3300
|
}
|
|
2951
3301
|
config.capture.profiles = [...activeNames];
|
|
@@ -3086,7 +3436,9 @@ function registerNote(program2) {
|
|
|
3086
3436
|
repo_name: repoName,
|
|
3087
3437
|
branch,
|
|
3088
3438
|
category: "note",
|
|
3089
|
-
metadata: JSON.stringify({ text })
|
|
3439
|
+
metadata: JSON.stringify({ text }),
|
|
3440
|
+
source: null,
|
|
3441
|
+
file_path: null
|
|
3090
3442
|
});
|
|
3091
3443
|
log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
|
|
3092
3444
|
});
|
|
@@ -3094,8 +3446,8 @@ function registerNote(program2) {
|
|
|
3094
3446
|
|
|
3095
3447
|
// src/cli/week.ts
|
|
3096
3448
|
var import_chalk11 = __toESM(require("chalk"));
|
|
3097
|
-
var
|
|
3098
|
-
var
|
|
3449
|
+
var import_fs21 = __toESM(require("fs"));
|
|
3450
|
+
var import_path15 = __toESM(require("path"));
|
|
3099
3451
|
var import_os8 = __toESM(require("os"));
|
|
3100
3452
|
function eventRowToPayload3(row) {
|
|
3101
3453
|
let metadata = {};
|
|
@@ -3168,9 +3520,9 @@ function registerWeek(program2) {
|
|
|
3168
3520
|
return;
|
|
3169
3521
|
}
|
|
3170
3522
|
const cacheKey = isoWeekKey(today);
|
|
3171
|
-
const cacheFile =
|
|
3172
|
-
if (!opts.fresh &&
|
|
3173
|
-
const cached =
|
|
3523
|
+
const cacheFile = import_path15.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
3524
|
+
if (!opts.fresh && import_fs21.default.existsSync(cacheFile)) {
|
|
3525
|
+
const cached = import_fs21.default.readFileSync(cacheFile, "utf-8");
|
|
3174
3526
|
log.plain("");
|
|
3175
3527
|
log.plain(cached);
|
|
3176
3528
|
log.plain("");
|
|
@@ -3186,8 +3538,8 @@ function registerWeek(program2) {
|
|
|
3186
3538
|
log.plain("");
|
|
3187
3539
|
log.plain(summary);
|
|
3188
3540
|
log.plain("");
|
|
3189
|
-
|
|
3190
|
-
|
|
3541
|
+
import_fs21.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
3542
|
+
import_fs21.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
3191
3543
|
await postSummary(opts, config, summary);
|
|
3192
3544
|
} catch (error) {
|
|
3193
3545
|
log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
|
|
@@ -3197,15 +3549,15 @@ function registerWeek(program2) {
|
|
|
3197
3549
|
}
|
|
3198
3550
|
|
|
3199
3551
|
// src/cli/bug-report.ts
|
|
3200
|
-
var
|
|
3552
|
+
var import_fs22 = __toESM(require("fs"));
|
|
3201
3553
|
var import_os9 = __toESM(require("os"));
|
|
3202
|
-
var
|
|
3554
|
+
var import_path16 = __toESM(require("path"));
|
|
3203
3555
|
var import_chalk12 = __toESM(require("chalk"));
|
|
3204
3556
|
var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
|
|
3205
3557
|
var DEBUG_LOG_TAIL_LINES = 100;
|
|
3206
3558
|
function tailFile(filePath, lines) {
|
|
3207
|
-
if (!
|
|
3208
|
-
const content =
|
|
3559
|
+
if (!import_fs22.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
|
|
3560
|
+
const content = import_fs22.default.readFileSync(filePath, "utf-8");
|
|
3209
3561
|
const allLines = content.split("\n");
|
|
3210
3562
|
return allLines.slice(-lines).join("\n");
|
|
3211
3563
|
}
|
|
@@ -3223,9 +3575,9 @@ function redactConfig() {
|
|
|
3223
3575
|
function registerBugReport(program2) {
|
|
3224
3576
|
program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
|
|
3225
3577
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3226
|
-
const reportPath =
|
|
3578
|
+
const reportPath = import_path16.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
|
|
3227
3579
|
const systemInfo = [
|
|
3228
|
-
`Workscribe version : ${"1.
|
|
3580
|
+
`Workscribe version : ${"1.13.0"}`,
|
|
3229
3581
|
`Node.js version : ${process.version}`,
|
|
3230
3582
|
`Platform : ${process.platform}`,
|
|
3231
3583
|
`Architecture : ${process.arch}`,
|
|
@@ -3247,17 +3599,17 @@ function registerBugReport(program2) {
|
|
|
3247
3599
|
tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
|
|
3248
3600
|
].join("\n");
|
|
3249
3601
|
try {
|
|
3250
|
-
if (!
|
|
3251
|
-
|
|
3602
|
+
if (!import_fs22.default.existsSync(WORKSCRIBE_DIR)) {
|
|
3603
|
+
import_fs22.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
3252
3604
|
}
|
|
3253
|
-
|
|
3605
|
+
import_fs22.default.writeFileSync(reportPath, report, { mode: 384 });
|
|
3254
3606
|
} catch (error) {
|
|
3255
3607
|
log.error(`Could not write report: ${error.message}`);
|
|
3256
3608
|
process.exit(1);
|
|
3257
3609
|
}
|
|
3258
3610
|
const params = new URLSearchParams({
|
|
3259
|
-
title: `Bug report \u2014 workscribe ${"1.
|
|
3260
|
-
body: `<!-- Attach ${
|
|
3611
|
+
title: `Bug report \u2014 workscribe ${"1.13.0"}`,
|
|
3612
|
+
body: `<!-- Attach ${import_path16.default.basename(reportPath)} or paste the relevant sections below -->
|
|
3261
3613
|
|
|
3262
3614
|
**Describe the issue:**
|
|
3263
3615
|
|
|
@@ -3275,19 +3627,119 @@ function registerBugReport(program2) {
|
|
|
3275
3627
|
});
|
|
3276
3628
|
}
|
|
3277
3629
|
|
|
3630
|
+
// src/cli/alerts.ts
|
|
3631
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
3632
|
+
function formatTimestamp(ms) {
|
|
3633
|
+
return new Date(ms).toLocaleString([], {
|
|
3634
|
+
month: "short",
|
|
3635
|
+
day: "numeric",
|
|
3636
|
+
hour: "2-digit",
|
|
3637
|
+
minute: "2-digit"
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
function registerAlerts(program2) {
|
|
3641
|
+
const alertsCmd = program2.command("alerts").description("Show and manage security alerts from AI session monitoring").option("--check", "Print warning if alerts are pending (used by shell hooks)").action((opts) => {
|
|
3642
|
+
if (opts.check) {
|
|
3643
|
+
if (hasPendingAlerts()) {
|
|
3644
|
+
log.plain(
|
|
3645
|
+
import_chalk13.default.yellow(" [workscribe] Possible credentials detected in an AI session.") + import_chalk13.default.dim(" Run workscribe alerts for details.")
|
|
3646
|
+
);
|
|
3647
|
+
}
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
const alerts = getPendingAlerts();
|
|
3651
|
+
if (alerts.length === 0) {
|
|
3652
|
+
log.plain("");
|
|
3653
|
+
log.plain(" No pending security alerts.");
|
|
3654
|
+
log.plain("");
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
log.plain("");
|
|
3658
|
+
log.plain(import_chalk13.default.bold(" Security Alerts") + import_chalk13.default.dim(` (${alerts.length} pending)`));
|
|
3659
|
+
log.plain("");
|
|
3660
|
+
for (const alert of alerts) {
|
|
3661
|
+
log.plain(` ${import_chalk13.default.yellow("!")} ${import_chalk13.default.dim(formatTimestamp(alert.created_at))} ${import_chalk13.default.cyan(alert.tool)}`);
|
|
3662
|
+
log.plain(` ${alert.context}`);
|
|
3663
|
+
if (alert.session_id) log.plain(` ${import_chalk13.default.dim("Session: " + alert.session_id)}`);
|
|
3664
|
+
log.plain("");
|
|
3665
|
+
}
|
|
3666
|
+
log.plain(import_chalk13.default.dim(" Run workscribe alerts clear to dismiss all alerts"));
|
|
3667
|
+
log.plain("");
|
|
3668
|
+
});
|
|
3669
|
+
alertsCmd.command("clear").description("Dismiss all pending security alerts").action(() => {
|
|
3670
|
+
const count = clearAlerts();
|
|
3671
|
+
if (count === 0) {
|
|
3672
|
+
log.plain(" No pending alerts to clear.");
|
|
3673
|
+
} else {
|
|
3674
|
+
log.plain(` ${count} alert${count === 1 ? "" : "s"} cleared.`);
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// src/cli/context.ts
|
|
3680
|
+
var import_path17 = __toESM(require("path"));
|
|
3681
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
3682
|
+
function printSessionContext(session, heading) {
|
|
3683
|
+
const ago = timeAgo(session.started_at);
|
|
3684
|
+
const branch = getSessionBranch(session.id);
|
|
3685
|
+
const durationMinutes = (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4;
|
|
3686
|
+
log.plain("");
|
|
3687
|
+
log.plain(` ${import_chalk14.default.bold(heading)}: ${import_chalk14.default.cyan(ago)}`);
|
|
3688
|
+
log.plain("");
|
|
3689
|
+
log.plain(` Date ${session.date}`);
|
|
3690
|
+
log.plain(` Duration ${formatDuration(durationMinutes)}`);
|
|
3691
|
+
if (branch) log.plain(` Branch ${import_chalk14.default.dim(branch)}`);
|
|
3692
|
+
log.plain(` Events ${session.event_count}`);
|
|
3693
|
+
if (session.summary) {
|
|
3694
|
+
log.plain("");
|
|
3695
|
+
for (const line of session.summary.split("\n")) {
|
|
3696
|
+
log.plain(` ${line}`);
|
|
3697
|
+
}
|
|
3698
|
+
} else {
|
|
3699
|
+
log.plain("");
|
|
3700
|
+
log.plain(import_chalk14.default.dim(` No summary \u2014 run: workscribe summary ${session.date}`));
|
|
3701
|
+
}
|
|
3702
|
+
log.plain("");
|
|
3703
|
+
}
|
|
3704
|
+
function registerContext(program2) {
|
|
3705
|
+
program2.command("context").description("Show the last session for the current repo").option("--file <path>", "narrow to last session that touched this file").action((opts) => {
|
|
3706
|
+
if (opts.file) {
|
|
3707
|
+
const normalizedFile = import_path17.default.normalize(opts.file);
|
|
3708
|
+
const session = getLastSessionTouchingFile(normalizedFile);
|
|
3709
|
+
if (!session) {
|
|
3710
|
+
log.plain(` No sessions found touching ${normalizedFile}`);
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
printSessionContext(session, `Last session touching ${import_path17.default.basename(normalizedFile)}`);
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
const { repoName } = getGitContext(process.cwd());
|
|
3717
|
+
if (!repoName) {
|
|
3718
|
+
log.plain(" Not in a git repository. Run workscribe context from a project directory.");
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const sessions = getRecentSessionsByRepo(repoName, 1);
|
|
3722
|
+
if (sessions.length === 0) {
|
|
3723
|
+
log.plain(` No sessions found for ${repoName}`);
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
printSessionContext(sessions[0], `Last session on ${repoName}`);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3278
3730
|
// src/utils/update-check.ts
|
|
3279
|
-
var
|
|
3731
|
+
var import_fs23 = __toESM(require("fs"));
|
|
3280
3732
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3281
3733
|
function readCache() {
|
|
3282
3734
|
try {
|
|
3283
|
-
return JSON.parse(
|
|
3735
|
+
return JSON.parse(import_fs23.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
|
|
3284
3736
|
} catch {
|
|
3285
3737
|
return null;
|
|
3286
3738
|
}
|
|
3287
3739
|
}
|
|
3288
3740
|
function writeCache(cache) {
|
|
3289
3741
|
try {
|
|
3290
|
-
|
|
3742
|
+
import_fs23.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
|
|
3291
3743
|
} catch {
|
|
3292
3744
|
}
|
|
3293
3745
|
}
|
|
@@ -3300,7 +3752,7 @@ function isNewer(latest, current) {
|
|
|
3300
3752
|
return latestPatch > currentPatch;
|
|
3301
3753
|
}
|
|
3302
3754
|
function checkForUpdate() {
|
|
3303
|
-
const current = "1.
|
|
3755
|
+
const current = "1.13.0";
|
|
3304
3756
|
const cache = readCache();
|
|
3305
3757
|
if (cache && isNewer(cache.latestVersion, current)) {
|
|
3306
3758
|
process.stderr.write(
|
|
@@ -3325,7 +3777,7 @@ function checkForUpdate() {
|
|
|
3325
3777
|
assertNodeVersion(process.versions.node);
|
|
3326
3778
|
checkForUpdate();
|
|
3327
3779
|
var program = new import_commander.Command();
|
|
3328
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
3780
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.13.0");
|
|
3329
3781
|
registerCapture(program);
|
|
3330
3782
|
registerHook(program);
|
|
3331
3783
|
registerInit(program);
|
|
@@ -3344,5 +3796,7 @@ registerNote(program);
|
|
|
3344
3796
|
registerWeek(program);
|
|
3345
3797
|
registerBugReport(program);
|
|
3346
3798
|
registerCompletions(program);
|
|
3799
|
+
registerAlerts(program);
|
|
3800
|
+
registerContext(program);
|
|
3347
3801
|
program.parse();
|
|
3348
3802
|
//# sourceMappingURL=index.js.map
|