@workscribe/cli 1.11.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 +1027 -225
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/shell/hook.bash +3 -0
- package/shell/hook.fish +37 -0
- package/shell/hook.zsh +3 -0
package/dist/index.js
CHANGED
|
@@ -27,37 +27,64 @@ 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
|
}
|
|
42
60
|
|
|
43
61
|
// src/capture/index.ts
|
|
44
|
-
var
|
|
62
|
+
var import_fs10 = __toESM(require("fs"));
|
|
45
63
|
|
|
46
64
|
// src/db/index.ts
|
|
47
65
|
var import_node_sqlite = require("node:sqlite");
|
|
48
|
-
var
|
|
66
|
+
var import_fs2 = __toESM(require("fs"));
|
|
49
67
|
|
|
50
68
|
// src/utils/paths.ts
|
|
51
69
|
var import_path = __toESM(require("path"));
|
|
52
70
|
var import_os = __toESM(require("os"));
|
|
53
|
-
var
|
|
54
|
-
var
|
|
55
|
-
var
|
|
56
|
-
var
|
|
57
|
-
var
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
var
|
|
71
|
+
var import_fs = __toESM(require("fs"));
|
|
72
|
+
var LEGACY_DIR = import_path.default.join(import_os.default.homedir(), ".workscribe");
|
|
73
|
+
var USE_XDG = !import_fs.default.existsSync(LEGACY_DIR);
|
|
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;
|
|
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;
|
|
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;
|
|
77
|
+
var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir, LEGACY_DIR])];
|
|
78
|
+
var WORKSCRIBE_DIR = xdgDataDir;
|
|
79
|
+
var DB_PATH = import_path.default.join(xdgDataDir, "workscribe.db");
|
|
80
|
+
var CONFIG_PATH = import_path.default.join(xdgConfigDir, "config.json");
|
|
81
|
+
var DEBUG_LOG_PATH = import_path.default.join(xdgStateDir, "debug.log");
|
|
82
|
+
var UPDATE_CHECK_PATH = import_path.default.join(xdgStateDir, "update-check.json");
|
|
83
|
+
var EXPORTS_DIR = import_path.default.join(xdgDataDir, "exports");
|
|
84
|
+
var PAUSE_FLAG_PATH = import_path.default.join(xdgStateDir, "paused");
|
|
85
|
+
var WEEKLY_CACHE_DIR = import_path.default.join(xdgDataDir, "weekly-cache");
|
|
86
|
+
var PROFILES_DIR = import_path.default.join(xdgDataDir, "profiles");
|
|
87
|
+
var ALERTS_PENDING_PATH = import_path.default.join(LEGACY_DIR, "alerts.pending");
|
|
61
88
|
|
|
62
89
|
// src/db/migrations/001_initial_schema.ts
|
|
63
90
|
var initial_schema_default = {
|
|
@@ -119,19 +146,49 @@ var migration = {
|
|
|
119
146
|
};
|
|
120
147
|
var semantic_categories_default = migration;
|
|
121
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
|
+
|
|
122
179
|
// src/db/migrations/index.ts
|
|
123
|
-
var MIGRATIONS = [initial_schema_default, semantic_categories_default];
|
|
180
|
+
var MIGRATIONS = [initial_schema_default, semantic_categories_default, alerts_default, ide_columns_default];
|
|
124
181
|
|
|
125
182
|
// src/db/index.ts
|
|
126
183
|
var _db = null;
|
|
127
184
|
function getDb() {
|
|
128
185
|
if (_db) return _db;
|
|
129
|
-
if (!
|
|
130
|
-
|
|
186
|
+
if (!import_fs2.default.existsSync(WORKSCRIBE_DIR)) {
|
|
187
|
+
import_fs2.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
|
|
131
188
|
}
|
|
132
189
|
const db = new import_node_sqlite.DatabaseSync(DB_PATH);
|
|
133
190
|
try {
|
|
134
|
-
|
|
191
|
+
import_fs2.default.chmodSync(DB_PATH, 384);
|
|
135
192
|
} catch {
|
|
136
193
|
}
|
|
137
194
|
db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -157,13 +214,13 @@ function applyMigrations(db) {
|
|
|
157
214
|
const applied = new Set(
|
|
158
215
|
db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
|
|
159
216
|
);
|
|
160
|
-
for (const
|
|
161
|
-
if (applied.has(
|
|
217
|
+
for (const migration4 of MIGRATIONS) {
|
|
218
|
+
if (applied.has(migration4.version)) continue;
|
|
162
219
|
db.exec("BEGIN");
|
|
163
220
|
try {
|
|
164
|
-
db.exec(
|
|
221
|
+
db.exec(migration4.sql);
|
|
165
222
|
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
|
|
166
|
-
version:
|
|
223
|
+
version: migration4.version
|
|
167
224
|
});
|
|
168
225
|
db.exec("COMMIT");
|
|
169
226
|
} catch (err) {
|
|
@@ -171,18 +228,48 @@ function applyMigrations(db) {
|
|
|
171
228
|
db.exec("ROLLBACK");
|
|
172
229
|
} catch {
|
|
173
230
|
}
|
|
174
|
-
throw new Error(`Migration ${
|
|
231
|
+
throw new Error(`Migration ${migration4.version} (${migration4.description}) failed: ${err.message}`);
|
|
175
232
|
}
|
|
176
233
|
}
|
|
177
234
|
}
|
|
178
235
|
function insertEvent(params) {
|
|
179
236
|
const db = getDb();
|
|
180
237
|
const stmt = db.prepare(`
|
|
181
|
-
INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata)
|
|
182
|
-
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)
|
|
183
240
|
`);
|
|
184
241
|
stmt.run(params);
|
|
185
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
|
+
}
|
|
186
273
|
function getLastEvent() {
|
|
187
274
|
const db = getDb();
|
|
188
275
|
const stmt = db.prepare("SELECT * FROM events ORDER BY id DESC LIMIT 1");
|
|
@@ -324,7 +411,10 @@ var BUILTIN_PATTERNS = [
|
|
|
324
411
|
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
|
|
325
412
|
// Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
|
|
326
413
|
// Lookbehind matches only the variable-name= prefix, so only the value is replaced
|
|
327
|
-
/(?<=[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
|
|
328
418
|
];
|
|
329
419
|
function redact(command, extraPatterns = []) {
|
|
330
420
|
let result = command;
|
|
@@ -345,7 +435,7 @@ function redact(command, extraPatterns = []) {
|
|
|
345
435
|
// src/capture/git.ts
|
|
346
436
|
var import_child_process = require("child_process");
|
|
347
437
|
var import_path2 = __toESM(require("path"));
|
|
348
|
-
var
|
|
438
|
+
var import_fs3 = __toESM(require("fs"));
|
|
349
439
|
function nameFromRemote(root) {
|
|
350
440
|
try {
|
|
351
441
|
const url = (0, import_child_process.execSync)("git remote get-url origin", {
|
|
@@ -361,7 +451,7 @@ function nameFromRemote(root) {
|
|
|
361
451
|
}
|
|
362
452
|
function nameFromPackageJson(root) {
|
|
363
453
|
try {
|
|
364
|
-
const raw =
|
|
454
|
+
const raw = import_fs3.default.readFileSync(import_path2.default.join(root, "package.json"), "utf-8");
|
|
365
455
|
const pkg = JSON.parse(raw);
|
|
366
456
|
if (!pkg.name) return null;
|
|
367
457
|
return pkg.name.replace(/^@[^/]+\//, "");
|
|
@@ -405,15 +495,15 @@ function getCommitsSince(sha, cwd) {
|
|
|
405
495
|
}
|
|
406
496
|
|
|
407
497
|
// src/capture/aider.ts
|
|
408
|
-
var
|
|
498
|
+
var import_fs4 = __toESM(require("fs"));
|
|
409
499
|
var import_path3 = __toESM(require("path"));
|
|
410
500
|
var HISTORY_FILE = ".aider.chat.history.md";
|
|
411
501
|
var PROMPT_MAX_LENGTH = 200;
|
|
412
502
|
function readLastAiderPrompt(cwd) {
|
|
413
503
|
try {
|
|
414
504
|
const histPath = import_path3.default.join(cwd, HISTORY_FILE);
|
|
415
|
-
const content =
|
|
416
|
-
const matches = [...content.matchAll(
|
|
505
|
+
const content = import_fs4.default.readFileSync(histPath, "utf-8");
|
|
506
|
+
const matches = [...content.matchAll(/^#### (.+)$/gm)];
|
|
417
507
|
if (!matches.length) return null;
|
|
418
508
|
return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
|
|
419
509
|
} catch {
|
|
@@ -422,7 +512,7 @@ function readLastAiderPrompt(cwd) {
|
|
|
422
512
|
}
|
|
423
513
|
|
|
424
514
|
// src/config/registry.ts
|
|
425
|
-
var
|
|
515
|
+
var import_fs6 = __toESM(require("fs"));
|
|
426
516
|
var import_os2 = __toESM(require("os"));
|
|
427
517
|
var import_path4 = __toESM(require("path"));
|
|
428
518
|
|
|
@@ -433,62 +523,114 @@ var ai_tools_default = [
|
|
|
433
523
|
displayName: "Claude Code",
|
|
434
524
|
pattern: "^claude\\b",
|
|
435
525
|
hook: "claude-code",
|
|
436
|
-
contextSource: "hook"
|
|
526
|
+
contextSource: "hook",
|
|
527
|
+
historyFormat: null,
|
|
528
|
+
securityWarning: null
|
|
437
529
|
},
|
|
438
530
|
{
|
|
439
531
|
tool: "aider",
|
|
440
532
|
displayName: "Aider",
|
|
441
533
|
pattern: "^aider\\b",
|
|
442
534
|
hook: null,
|
|
443
|
-
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
|
|
444
547
|
},
|
|
445
548
|
{
|
|
446
549
|
tool: "gemini",
|
|
447
|
-
displayName: "Gemini",
|
|
550
|
+
displayName: "Gemini CLI",
|
|
448
551
|
pattern: "^gemini\\b",
|
|
449
552
|
hook: null,
|
|
450
|
-
contextSource:
|
|
553
|
+
contextSource: "history-file",
|
|
554
|
+
historyFormat: "json",
|
|
555
|
+
securityWarning: null
|
|
451
556
|
},
|
|
452
557
|
{
|
|
453
558
|
tool: "gh copilot",
|
|
454
559
|
displayName: "GitHub Copilot",
|
|
455
560
|
pattern: "^gh\\s+copilot\\b",
|
|
456
561
|
hook: null,
|
|
457
|
-
contextSource:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
tool: "cody",
|
|
461
|
-
displayName: "Cody",
|
|
462
|
-
pattern: "^cody\\b",
|
|
463
|
-
hook: null,
|
|
464
|
-
contextSource: null
|
|
562
|
+
contextSource: "history-file",
|
|
563
|
+
historyFormat: "jsonl",
|
|
564
|
+
securityWarning: null
|
|
465
565
|
},
|
|
466
566
|
{
|
|
467
567
|
tool: "amazon-q",
|
|
468
568
|
displayName: "Amazon Q",
|
|
469
569
|
pattern: "^q\\s+(chat|dev)\\b",
|
|
470
570
|
hook: null,
|
|
471
|
-
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."
|
|
472
601
|
},
|
|
473
602
|
{
|
|
474
603
|
tool: "continue",
|
|
475
604
|
displayName: "Continue",
|
|
476
605
|
pattern: "^continue\\b",
|
|
477
606
|
hook: null,
|
|
478
|
-
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
|
|
479
619
|
},
|
|
480
620
|
{
|
|
481
621
|
tool: "cursor",
|
|
482
622
|
displayName: "Cursor",
|
|
483
623
|
pattern: "^cursor\\b",
|
|
484
624
|
hook: null,
|
|
485
|
-
contextSource: null
|
|
625
|
+
contextSource: null,
|
|
626
|
+
historyFormat: null,
|
|
627
|
+
securityWarning: null
|
|
486
628
|
}
|
|
487
629
|
];
|
|
488
630
|
|
|
489
631
|
// src/utils/logger.ts
|
|
490
632
|
var import_chalk = __toESM(require("chalk"));
|
|
491
|
-
var
|
|
633
|
+
var import_fs5 = __toESM(require("fs"));
|
|
492
634
|
var isDebug = process.env.WORKSCRIBE_DEBUG === "1";
|
|
493
635
|
var log = {
|
|
494
636
|
info: (msg) => console.log(import_chalk.default.cyan("\u2192"), msg),
|
|
@@ -502,7 +644,7 @@ function debugLog(context, error) {
|
|
|
502
644
|
const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${context}] ${error instanceof Error ? error.stack : String(error)}
|
|
503
645
|
`;
|
|
504
646
|
try {
|
|
505
|
-
|
|
647
|
+
import_fs5.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
|
|
506
648
|
} catch {
|
|
507
649
|
}
|
|
508
650
|
}
|
|
@@ -512,8 +654,8 @@ var USER_REGISTRY_PATH = import_path4.default.join(import_os2.default.homedir(),
|
|
|
512
654
|
function loadAIToolRegistry() {
|
|
513
655
|
let userEntries = [];
|
|
514
656
|
try {
|
|
515
|
-
if (
|
|
516
|
-
const raw =
|
|
657
|
+
if (import_fs6.default.existsSync(USER_REGISTRY_PATH)) {
|
|
658
|
+
const raw = import_fs6.default.readFileSync(USER_REGISTRY_PATH, "utf-8");
|
|
517
659
|
const parsed = JSON.parse(raw);
|
|
518
660
|
if (Array.isArray(parsed)) {
|
|
519
661
|
userEntries = parsed.filter((entry) => typeof entry.tool === "string" && typeof entry.pattern === "string").map((entry) => ({
|
|
@@ -521,7 +663,9 @@ function loadAIToolRegistry() {
|
|
|
521
663
|
displayName: entry.displayName ?? entry.tool,
|
|
522
664
|
pattern: entry.pattern,
|
|
523
665
|
hook: entry.hook ?? null,
|
|
524
|
-
contextSource: entry.contextSource ?? null
|
|
666
|
+
contextSource: entry.contextSource ?? null,
|
|
667
|
+
historyFormat: entry.historyFormat ?? null,
|
|
668
|
+
securityWarning: entry.securityWarning ?? null
|
|
525
669
|
}));
|
|
526
670
|
}
|
|
527
671
|
}
|
|
@@ -535,11 +679,11 @@ function loadAIToolRegistry() {
|
|
|
535
679
|
}
|
|
536
680
|
|
|
537
681
|
// src/profiles/loader.ts
|
|
538
|
-
var
|
|
682
|
+
var import_fs9 = __toESM(require("fs"));
|
|
539
683
|
var import_path6 = __toESM(require("path"));
|
|
540
684
|
|
|
541
685
|
// src/config/index.ts
|
|
542
|
-
var
|
|
686
|
+
var import_fs7 = __toESM(require("fs"));
|
|
543
687
|
var DEFAULTS = {
|
|
544
688
|
ai: {
|
|
545
689
|
provider: "",
|
|
@@ -564,9 +708,9 @@ var DEFAULTS = {
|
|
|
564
708
|
}
|
|
565
709
|
};
|
|
566
710
|
function readConfig() {
|
|
567
|
-
if (!
|
|
711
|
+
if (!import_fs7.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
568
712
|
try {
|
|
569
|
-
const raw =
|
|
713
|
+
const raw = import_fs7.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
570
714
|
const parsed = JSON.parse(raw);
|
|
571
715
|
return deepMerge(DEFAULTS, parsed);
|
|
572
716
|
} catch {
|
|
@@ -574,9 +718,9 @@ function readConfig() {
|
|
|
574
718
|
}
|
|
575
719
|
}
|
|
576
720
|
function writeConfig(config) {
|
|
577
|
-
|
|
721
|
+
import_fs7.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
578
722
|
try {
|
|
579
|
-
|
|
723
|
+
import_fs7.default.chmodSync(CONFIG_PATH, 384);
|
|
580
724
|
} catch {
|
|
581
725
|
}
|
|
582
726
|
}
|
|
@@ -641,21 +785,57 @@ function isValidManifest(m) {
|
|
|
641
785
|
}
|
|
642
786
|
|
|
643
787
|
// src/profiles/fetcher.ts
|
|
644
|
-
var
|
|
788
|
+
var import_fs8 = __toESM(require("fs"));
|
|
645
789
|
var import_path5 = __toESM(require("path"));
|
|
790
|
+
|
|
791
|
+
// src/utils/fetch.ts
|
|
792
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
793
|
+
var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
794
|
+
"ENOTFOUND",
|
|
795
|
+
"ECONNREFUSED",
|
|
796
|
+
"ECONNRESET",
|
|
797
|
+
"ETIMEDOUT",
|
|
798
|
+
"EHOSTUNREACH",
|
|
799
|
+
"ENETUNREACH"
|
|
800
|
+
]);
|
|
801
|
+
function isNetworkError(err) {
|
|
802
|
+
if (!(err instanceof Error)) return false;
|
|
803
|
+
const cause = err.cause;
|
|
804
|
+
const code = err.code ?? (cause instanceof Error ? cause.code : void 0);
|
|
805
|
+
return NETWORK_ERROR_CODES.has(code ?? "");
|
|
806
|
+
}
|
|
807
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
808
|
+
const controller = new AbortController();
|
|
809
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
810
|
+
try {
|
|
811
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
812
|
+
} catch (err) {
|
|
813
|
+
if (err.name === "AbortError") {
|
|
814
|
+
throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
|
|
815
|
+
}
|
|
816
|
+
if (isNetworkError(err)) {
|
|
817
|
+
throw new Error("No internet connection \u2014 try again when you're online");
|
|
818
|
+
}
|
|
819
|
+
throw err;
|
|
820
|
+
} finally {
|
|
821
|
+
clearTimeout(timer);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/profiles/fetcher.ts
|
|
646
826
|
var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
|
|
647
827
|
function manifestCachePath() {
|
|
648
828
|
return import_path5.default.join(PROFILES_DIR, "manifest.json");
|
|
649
829
|
}
|
|
650
830
|
async function fetchManifest() {
|
|
651
|
-
const response = await
|
|
831
|
+
const response = await fetchWithTimeout(`${REGISTRY_BASE}/manifest.json`, {});
|
|
652
832
|
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
653
833
|
const data = await response.json();
|
|
654
834
|
if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
|
|
655
835
|
return data;
|
|
656
836
|
}
|
|
657
837
|
async function fetchProfile(name) {
|
|
658
|
-
const response = await
|
|
838
|
+
const response = await fetchWithTimeout(`${REGISTRY_BASE}/${name}.json`, {});
|
|
659
839
|
if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
|
|
660
840
|
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
661
841
|
const data = await response.json();
|
|
@@ -666,27 +846,27 @@ async function installProfile(name) {
|
|
|
666
846
|
ensureProfilesDir();
|
|
667
847
|
const profile = await fetchProfile(name);
|
|
668
848
|
const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
|
|
669
|
-
|
|
849
|
+
import_fs8.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
|
|
670
850
|
debugLog("profiles", `Installed profile "${name}" to ${dest}`);
|
|
671
851
|
}
|
|
672
852
|
async function cacheManifest() {
|
|
673
853
|
ensureProfilesDir();
|
|
674
854
|
const manifest = await fetchManifest();
|
|
675
|
-
|
|
855
|
+
import_fs8.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
|
|
676
856
|
return manifest;
|
|
677
857
|
}
|
|
678
858
|
function readCachedManifest() {
|
|
679
|
-
if (!
|
|
859
|
+
if (!import_fs8.default.existsSync(manifestCachePath())) return null;
|
|
680
860
|
try {
|
|
681
|
-
const data = JSON.parse(
|
|
861
|
+
const data = JSON.parse(import_fs8.default.readFileSync(manifestCachePath(), "utf-8"));
|
|
682
862
|
return isValidManifest(data) ? data : null;
|
|
683
863
|
} catch {
|
|
684
864
|
return null;
|
|
685
865
|
}
|
|
686
866
|
}
|
|
687
867
|
function ensureProfilesDir() {
|
|
688
|
-
if (!
|
|
689
|
-
|
|
868
|
+
if (!import_fs8.default.existsSync(PROFILES_DIR)) {
|
|
869
|
+
import_fs8.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
690
870
|
}
|
|
691
871
|
}
|
|
692
872
|
|
|
@@ -713,13 +893,13 @@ function compileRule(profileName, rule) {
|
|
|
713
893
|
}
|
|
714
894
|
function loadProfileFromDisk(name) {
|
|
715
895
|
const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
|
|
716
|
-
if (!
|
|
896
|
+
if (!import_fs9.default.existsSync(filePath)) {
|
|
717
897
|
debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
|
|
718
898
|
return null;
|
|
719
899
|
}
|
|
720
900
|
let candidate;
|
|
721
901
|
try {
|
|
722
|
-
candidate = JSON.parse(
|
|
902
|
+
candidate = JSON.parse(import_fs9.default.readFileSync(filePath, "utf-8"));
|
|
723
903
|
} catch {
|
|
724
904
|
debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
|
|
725
905
|
return null;
|
|
@@ -751,8 +931,8 @@ function getActiveProfileAiContexts() {
|
|
|
751
931
|
});
|
|
752
932
|
}
|
|
753
933
|
function getCachedProfiles() {
|
|
754
|
-
if (!
|
|
755
|
-
return
|
|
934
|
+
if (!import_fs9.default.existsSync(PROFILES_DIR)) return [];
|
|
935
|
+
return import_fs9.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
|
|
756
936
|
const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
|
|
757
937
|
return profile ? [profile] : [];
|
|
758
938
|
});
|
|
@@ -764,7 +944,7 @@ function detectProfilesForDirectory(dir) {
|
|
|
764
944
|
return manifest.filter((entry) => {
|
|
765
945
|
if (activeNames.has(entry.name)) return false;
|
|
766
946
|
const indicatorFiles = entry.detect?.files ?? [];
|
|
767
|
-
return indicatorFiles.length > 0 && indicatorFiles.every((file) =>
|
|
947
|
+
return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs9.default.existsSync(import_path6.default.join(dir, file)));
|
|
768
948
|
});
|
|
769
949
|
}
|
|
770
950
|
|
|
@@ -1030,11 +1210,41 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
|
|
|
1030
1210
|
return lastEvent.command === normalizeForFilter(command) && lastEvent.cwd === cwd && now - lastTime < dedupWindowMs;
|
|
1031
1211
|
}
|
|
1032
1212
|
|
|
1213
|
+
// src/capture/ide.ts
|
|
1214
|
+
var IDE_DEDUP_WINDOW_MS = 3e4;
|
|
1215
|
+
|
|
1033
1216
|
// src/capture/index.ts
|
|
1034
1217
|
function captureCommand(opts) {
|
|
1035
|
-
if (
|
|
1218
|
+
if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
|
|
1036
1219
|
const config = readConfig();
|
|
1037
|
-
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;
|
|
1038
1248
|
if (shouldFilter(command, config.capture.ignore)) return;
|
|
1039
1249
|
const last = getLastEvent();
|
|
1040
1250
|
if (isDuplicate(command, cwd, last ?? void 0)) return;
|
|
@@ -1048,7 +1258,6 @@ function captureCommand(opts) {
|
|
|
1048
1258
|
if (firstToken) metadata.tool = firstToken;
|
|
1049
1259
|
metadata.hint = safeCommand.trim().slice(0, 120);
|
|
1050
1260
|
}
|
|
1051
|
-
if (opts.source) metadata.source = opts.source;
|
|
1052
1261
|
if (category === "ai_session") {
|
|
1053
1262
|
if (opts.gitBefore) {
|
|
1054
1263
|
const commits = getCommitsSince(opts.gitBefore, cwd);
|
|
@@ -1071,24 +1280,30 @@ function captureCommand(opts) {
|
|
|
1071
1280
|
repo_name: repoName,
|
|
1072
1281
|
branch,
|
|
1073
1282
|
category,
|
|
1074
|
-
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
|
|
1075
1286
|
});
|
|
1076
1287
|
}
|
|
1077
1288
|
|
|
1078
1289
|
// src/cli/capture.ts
|
|
1079
1290
|
var CAPTURE_HANG_TIMEOUT_MS = 5e3;
|
|
1080
1291
|
function registerCapture(program2) {
|
|
1081
|
-
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) => {
|
|
1082
1293
|
const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
|
|
1083
1294
|
hangGuard.unref();
|
|
1084
1295
|
try {
|
|
1085
|
-
if (!opts.
|
|
1296
|
+
if (!opts.cwd) return;
|
|
1297
|
+
if (!opts.cmd && !opts.type) return;
|
|
1086
1298
|
captureCommand({
|
|
1087
1299
|
command: opts.cmd,
|
|
1088
1300
|
cwd: opts.cwd,
|
|
1089
1301
|
exitCode: opts.exit ?? null,
|
|
1090
1302
|
durationMs: opts.duration ?? null,
|
|
1091
|
-
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
|
|
1092
1307
|
});
|
|
1093
1308
|
} catch (err) {
|
|
1094
1309
|
debugLog("_capture", err);
|
|
@@ -1096,6 +1311,38 @@ function registerCapture(program2) {
|
|
|
1096
1311
|
});
|
|
1097
1312
|
}
|
|
1098
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
|
+
|
|
1099
1346
|
// src/cli/hook.ts
|
|
1100
1347
|
function parseHookPayload(raw) {
|
|
1101
1348
|
if (!raw.trim()) return null;
|
|
@@ -1106,8 +1353,17 @@ function parseHookPayload(raw) {
|
|
|
1106
1353
|
if (!command || !cwd) return null;
|
|
1107
1354
|
return { command, cwd };
|
|
1108
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
|
+
}
|
|
1109
1365
|
function registerHook(program2) {
|
|
1110
|
-
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) => {
|
|
1111
1367
|
let raw = "";
|
|
1112
1368
|
process.stdin.setEncoding("utf-8");
|
|
1113
1369
|
process.stdin.on("data", (chunk) => {
|
|
@@ -1115,6 +1371,23 @@ function registerHook(program2) {
|
|
|
1115
1371
|
});
|
|
1116
1372
|
process.stdin.on("end", () => {
|
|
1117
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
|
+
}
|
|
1118
1391
|
if (readConfig().capture.aiTools === false) return;
|
|
1119
1392
|
const parsed = parseHookPayload(raw);
|
|
1120
1393
|
if (!parsed) return;
|
|
@@ -1133,9 +1406,9 @@ function registerHook(program2) {
|
|
|
1133
1406
|
}
|
|
1134
1407
|
|
|
1135
1408
|
// src/cli/init.ts
|
|
1136
|
-
var
|
|
1137
|
-
var
|
|
1138
|
-
var
|
|
1409
|
+
var import_fs14 = __toESM(require("fs"));
|
|
1410
|
+
var import_path10 = __toESM(require("path"));
|
|
1411
|
+
var import_os5 = __toESM(require("os"));
|
|
1139
1412
|
var import_prompts = __toESM(require("prompts"));
|
|
1140
1413
|
var import_chalk2 = __toESM(require("chalk"));
|
|
1141
1414
|
|
|
@@ -1180,6 +1453,17 @@ function eachDayOfRange(start, end) {
|
|
|
1180
1453
|
}
|
|
1181
1454
|
return days;
|
|
1182
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
|
+
}
|
|
1183
1467
|
function isoWeekKey(dateStr) {
|
|
1184
1468
|
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1185
1469
|
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
@@ -1228,6 +1512,8 @@ RULES \u2014 follow these exactly:
|
|
|
1228
1512
|
|
|
1229
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.
|
|
1230
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
|
+
|
|
1231
1517
|
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1232
1518
|
function getSystemPrompt() {
|
|
1233
1519
|
const contexts = getActiveProfileAiContexts();
|
|
@@ -1269,7 +1555,9 @@ function buildUserMessage(payload, includeCategories = []) {
|
|
|
1269
1555
|
if (excluded.has(event.category)) continue;
|
|
1270
1556
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1271
1557
|
let key;
|
|
1272
|
-
if (event.
|
|
1558
|
+
if (event.category === "code_edit") {
|
|
1559
|
+
key = `code_edit${failed}`;
|
|
1560
|
+
} else if (event.hint) {
|
|
1273
1561
|
key = `other [${event.hint}]${failed}`;
|
|
1274
1562
|
} else {
|
|
1275
1563
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
@@ -1337,7 +1625,9 @@ function buildWeeklyMessage(payload) {
|
|
|
1337
1625
|
if (excluded.has(event.category)) continue;
|
|
1338
1626
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1339
1627
|
let key;
|
|
1340
|
-
if (event.
|
|
1628
|
+
if (event.category === "code_edit") {
|
|
1629
|
+
key = `code_edit${failed}`;
|
|
1630
|
+
} else if (event.hint) {
|
|
1341
1631
|
key = `other [${event.hint}]${failed}`;
|
|
1342
1632
|
} else {
|
|
1343
1633
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
@@ -1353,24 +1643,7 @@ function buildWeeklyMessage(payload) {
|
|
|
1353
1643
|
return lines.join("\n");
|
|
1354
1644
|
}
|
|
1355
1645
|
|
|
1356
|
-
// src/ai/
|
|
1357
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1358
|
-
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1359
|
-
const controller = new AbortController();
|
|
1360
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1361
|
-
try {
|
|
1362
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
1363
|
-
} catch (err) {
|
|
1364
|
-
if (err.name === "AbortError") {
|
|
1365
|
-
throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
|
|
1366
|
-
}
|
|
1367
|
-
throw err;
|
|
1368
|
-
} finally {
|
|
1369
|
-
clearTimeout(timer);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// src/ai/providers/index.ts
|
|
1646
|
+
// src/ai/generic-provider.ts
|
|
1374
1647
|
var GenericProvider = class {
|
|
1375
1648
|
constructor(config, model, apiKey, baseUrl, includeCategories = []) {
|
|
1376
1649
|
this.config = config;
|
|
@@ -1385,7 +1658,7 @@ var GenericProvider = class {
|
|
|
1385
1658
|
baseUrl;
|
|
1386
1659
|
includeCategories;
|
|
1387
1660
|
async call(system, userMsg, maxTokens) {
|
|
1388
|
-
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
|
|
1661
|
+
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl, this.apiKey, this.model), {
|
|
1389
1662
|
method: "POST",
|
|
1390
1663
|
headers: this.config.headers(this.apiKey),
|
|
1391
1664
|
body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
|
|
@@ -1403,7 +1676,7 @@ var GenericProvider = class {
|
|
|
1403
1676
|
}
|
|
1404
1677
|
async validate() {
|
|
1405
1678
|
const isPost = this.config.validateMethod === "POST";
|
|
1406
|
-
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
|
|
1679
|
+
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl, this.apiKey, this.model), {
|
|
1407
1680
|
method: this.config.validateMethod,
|
|
1408
1681
|
headers: isPost ? this.config.headers(this.apiKey) : void 0,
|
|
1409
1682
|
body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
|
|
@@ -1414,7 +1687,7 @@ var GenericProvider = class {
|
|
|
1414
1687
|
}
|
|
1415
1688
|
};
|
|
1416
1689
|
|
|
1417
|
-
// src/ai/
|
|
1690
|
+
// src/ai/provider-config.ts
|
|
1418
1691
|
var PROVIDER_CONFIGS = {
|
|
1419
1692
|
anthropic: {
|
|
1420
1693
|
defaultModel: "claude-haiku-4-5-20251001",
|
|
@@ -1502,6 +1775,59 @@ var PROVIDER_CONFIGS = {
|
|
|
1502
1775
|
},
|
|
1503
1776
|
chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
|
|
1504
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}`
|
|
1505
1831
|
}
|
|
1506
1832
|
};
|
|
1507
1833
|
|
|
@@ -1510,7 +1836,7 @@ function getProvider(config) {
|
|
|
1510
1836
|
const { provider, apiKey = "", model, baseUrl } = config.ai;
|
|
1511
1837
|
if (!provider) {
|
|
1512
1838
|
throw new Error(
|
|
1513
|
-
"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>"
|
|
1514
1840
|
);
|
|
1515
1841
|
}
|
|
1516
1842
|
const configKey = provider === "openai-compatible" ? "openai" : provider;
|
|
@@ -1518,7 +1844,7 @@ function getProvider(config) {
|
|
|
1518
1844
|
if (!providerConfig) {
|
|
1519
1845
|
throw new Error(
|
|
1520
1846
|
`Unknown provider: "${provider}"
|
|
1521
|
-
Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
1847
|
+
Supported providers: anthropic, openai, gemini, groq, ollama, openai-compatible`
|
|
1522
1848
|
);
|
|
1523
1849
|
}
|
|
1524
1850
|
if (provider !== "ollama" && !apiKey) {
|
|
@@ -1538,14 +1864,73 @@ Run: workscribe config set ai.apiKey <your-key>`);
|
|
|
1538
1864
|
}
|
|
1539
1865
|
|
|
1540
1866
|
// src/capture/shell-hook.ts
|
|
1541
|
-
var
|
|
1542
|
-
var
|
|
1867
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1868
|
+
var import_path8 = __toESM(require("path"));
|
|
1869
|
+
var import_os3 = __toESM(require("os"));
|
|
1870
|
+
var import_child_process3 = require("child_process");
|
|
1871
|
+
|
|
1872
|
+
// src/utils/completion-scripts.ts
|
|
1873
|
+
function bashCompletion(commands) {
|
|
1874
|
+
const names = commands.map((command) => command.name).join(" ");
|
|
1875
|
+
return `_workscribe_completion() {
|
|
1876
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1877
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1878
|
+
local commands="${names}"
|
|
1879
|
+
case "$prev" in
|
|
1880
|
+
completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
1881
|
+
config) COMPREPLY=( $(compgen -W "get set list" -- "$cur") ); return ;;
|
|
1882
|
+
profile) COMPREPLY=( $(compgen -W "list add remove update detect suggest" -- "$cur") ); return ;;
|
|
1883
|
+
esac
|
|
1884
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
1885
|
+
}
|
|
1886
|
+
complete -F _workscribe_completion workscribe`;
|
|
1887
|
+
}
|
|
1888
|
+
function zshCompletion(commands) {
|
|
1889
|
+
const commandList = commands.map((command) => ` '${command.name}:${command.description}'`).join("\n");
|
|
1890
|
+
return `#compdef workscribe
|
|
1891
|
+
_workscribe() {
|
|
1892
|
+
local state
|
|
1893
|
+
_arguments '1: :->command' '*: :->args'
|
|
1894
|
+
case $state in
|
|
1895
|
+
command)
|
|
1896
|
+
local -a commands
|
|
1897
|
+
commands=(
|
|
1898
|
+
${commandList}
|
|
1899
|
+
)
|
|
1900
|
+
_describe 'command' commands
|
|
1901
|
+
;;
|
|
1902
|
+
args)
|
|
1903
|
+
case $words[2] in
|
|
1904
|
+
completions) local -a shells; shells=('bash' 'zsh' 'fish'); _describe 'shell' shells ;;
|
|
1905
|
+
config) local -a sub; sub=('get:Get a value' 'set:Set a value' 'list:List all values'); _describe 'subcommand' sub ;;
|
|
1906
|
+
profile) local -a sub; sub=('list' 'add' 'remove' 'update' 'detect' 'suggest'); _describe 'subcommand' sub ;;
|
|
1907
|
+
esac
|
|
1908
|
+
;;
|
|
1909
|
+
esac
|
|
1910
|
+
}
|
|
1911
|
+
_workscribe`;
|
|
1912
|
+
}
|
|
1913
|
+
function fishCompletion(commands) {
|
|
1914
|
+
const lines = [
|
|
1915
|
+
"complete -c workscribe -f",
|
|
1916
|
+
...commands.map(
|
|
1917
|
+
(command) => `complete -c workscribe -n "__fish_use_subcommand" -a ${command.name.padEnd(12)} -d "${command.description}"`
|
|
1918
|
+
),
|
|
1919
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"',
|
|
1920
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from config" -a "get set list"',
|
|
1921
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from profile" -a "list add remove update detect suggest"'
|
|
1922
|
+
];
|
|
1923
|
+
return lines.join("\n");
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/capture/shell-hook.ts
|
|
1927
|
+
var FISH_COMPLETIONS_DIR = import_path8.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
|
|
1543
1928
|
var HOOK_START = "# workscribe:hook:start";
|
|
1544
1929
|
var HOOK_END = "# workscribe:hook:end";
|
|
1545
1930
|
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
1546
1931
|
function getInstalledHookBlock(rcFile) {
|
|
1547
|
-
if (!
|
|
1548
|
-
const content =
|
|
1932
|
+
if (!import_fs12.default.existsSync(rcFile)) return null;
|
|
1933
|
+
const content = import_fs12.default.readFileSync(rcFile, "utf-8");
|
|
1549
1934
|
const startIdx = content.indexOf(HOOK_START);
|
|
1550
1935
|
const endIdx = content.indexOf(HOOK_END);
|
|
1551
1936
|
if (startIdx === -1 || endIdx === -1) return null;
|
|
@@ -1561,45 +1946,88 @@ function isHookOutdated(rcFile, hookContent) {
|
|
|
1561
1946
|
}
|
|
1562
1947
|
function installHook(rcFile, hookContent) {
|
|
1563
1948
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1564
|
-
|
|
1565
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1566
|
-
|
|
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, `
|
|
1567
1952
|
${hookContent}
|
|
1568
1953
|
`);
|
|
1569
1954
|
}
|
|
1570
1955
|
function updateHook(rcFile, hookContent) {
|
|
1571
1956
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1572
|
-
|
|
1573
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1574
|
-
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");
|
|
1575
1960
|
const startIdx = content.indexOf(HOOK_START);
|
|
1576
1961
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1577
|
-
|
|
1962
|
+
import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
1578
1963
|
}
|
|
1579
1964
|
function removeHook(rcFile) {
|
|
1580
1965
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1581
|
-
|
|
1582
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
1583
|
-
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");
|
|
1584
1969
|
const startIdx = content.indexOf(HOOK_START);
|
|
1585
1970
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1586
1971
|
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
1587
1972
|
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
1588
1973
|
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
1589
1974
|
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
1590
|
-
|
|
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`);
|
|
1988
|
+
}
|
|
1989
|
+
function isCompletionsInstalled(shell) {
|
|
1990
|
+
if (shell.name === "fish") {
|
|
1991
|
+
return import_fs12.default.existsSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
|
|
1995
|
+
} catch {
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function installCompletions(shell, commands) {
|
|
2000
|
+
if (shell.name === "fish") {
|
|
2001
|
+
if (!import_fs12.default.existsSync(FISH_COMPLETIONS_DIR)) {
|
|
2002
|
+
import_fs12.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
|
|
2003
|
+
}
|
|
2004
|
+
import_fs12.default.writeFileSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
|
|
2005
|
+
} else {
|
|
2006
|
+
const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
|
|
2007
|
+
const cacheFile = import_path8.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
|
|
2008
|
+
import_fs12.default.writeFileSync(cacheFile, script, { mode: 420 });
|
|
2009
|
+
const sourceLine = shell.name === "zsh" ? `
|
|
2010
|
+
# Workscribe completions
|
|
2011
|
+
autoload -Uz compinit && compinit
|
|
2012
|
+
source "${cacheFile}"
|
|
2013
|
+
` : `
|
|
2014
|
+
# Workscribe completions
|
|
2015
|
+
source "${cacheFile}"
|
|
2016
|
+
`;
|
|
2017
|
+
import_fs12.default.appendFileSync(shell.rcFile, sourceLine);
|
|
2018
|
+
}
|
|
1591
2019
|
}
|
|
1592
2020
|
|
|
1593
2021
|
// src/capture/hooks/claude-code.ts
|
|
1594
|
-
var
|
|
1595
|
-
var
|
|
1596
|
-
var
|
|
2022
|
+
var import_fs13 = __toESM(require("fs"));
|
|
2023
|
+
var import_path9 = __toESM(require("path"));
|
|
2024
|
+
var import_os4 = __toESM(require("os"));
|
|
1597
2025
|
|
|
1598
2026
|
// src/capture/hooks/utils.ts
|
|
1599
|
-
var
|
|
2027
|
+
var import_child_process4 = require("child_process");
|
|
1600
2028
|
function isCommandInstalled(cmd) {
|
|
1601
2029
|
try {
|
|
1602
|
-
(0,
|
|
2030
|
+
(0, import_child_process4.execSync)(`which ${cmd}`, { stdio: "ignore" });
|
|
1603
2031
|
return true;
|
|
1604
2032
|
} catch {
|
|
1605
2033
|
return false;
|
|
@@ -1607,11 +2035,11 @@ function isCommandInstalled(cmd) {
|
|
|
1607
2035
|
}
|
|
1608
2036
|
|
|
1609
2037
|
// src/capture/hooks/claude-code.ts
|
|
1610
|
-
var CLAUDE_SETTINGS_PATH =
|
|
2038
|
+
var CLAUDE_SETTINGS_PATH = import_path9.default.join(import_os4.default.homedir(), ".claude", "settings.json");
|
|
1611
2039
|
function readSettings() {
|
|
1612
|
-
if (!
|
|
2040
|
+
if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
1613
2041
|
try {
|
|
1614
|
-
return JSON.parse(
|
|
2042
|
+
return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
1615
2043
|
} catch {
|
|
1616
2044
|
return {};
|
|
1617
2045
|
}
|
|
@@ -1623,19 +2051,30 @@ var claudeCodeIntegration = {
|
|
|
1623
2051
|
},
|
|
1624
2052
|
isHookInstalled() {
|
|
1625
2053
|
const settings = readSettings();
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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) => {
|
|
1630
2059
|
const hook = hookItem;
|
|
1631
2060
|
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
1632
2061
|
});
|
|
1633
2062
|
});
|
|
2063
|
+
const userPromptSubmit = hooks.UserPromptSubmit ?? [];
|
|
2064
|
+
const hasUserPromptSubmit = userPromptSubmit.some((entry) => {
|
|
2065
|
+
const entryHooks = entry.hooks ?? [];
|
|
2066
|
+
return entryHooks.some((hookItem) => {
|
|
2067
|
+
const hook = hookItem;
|
|
2068
|
+
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
return hasPostToolUse && hasUserPromptSubmit;
|
|
1634
2072
|
},
|
|
1635
2073
|
install() {
|
|
1636
2074
|
const settings = readSettings();
|
|
1637
2075
|
const existingHooks = settings.hooks ?? {};
|
|
1638
2076
|
const existingPostToolUse = existingHooks.PostToolUse ?? [];
|
|
2077
|
+
const existingUserPromptSubmit = existingHooks.UserPromptSubmit ?? [];
|
|
1639
2078
|
settings.hooks = {
|
|
1640
2079
|
...existingHooks,
|
|
1641
2080
|
PostToolUse: [
|
|
@@ -1644,10 +2083,21 @@ var claudeCodeIntegration = {
|
|
|
1644
2083
|
matcher: "Bash",
|
|
1645
2084
|
hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
|
|
1646
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
|
+
}
|
|
1647
2097
|
]
|
|
1648
2098
|
};
|
|
1649
|
-
|
|
1650
|
-
|
|
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));
|
|
1651
2101
|
}
|
|
1652
2102
|
};
|
|
1653
2103
|
|
|
@@ -1656,11 +2106,34 @@ var AI_HOOK_INTEGRATIONS = {
|
|
|
1656
2106
|
"claude-code": claudeCodeIntegration
|
|
1657
2107
|
};
|
|
1658
2108
|
|
|
2109
|
+
// src/cli/completions.ts
|
|
2110
|
+
var HIDDEN_COMMANDS = /* @__PURE__ */ new Set(["_capture", "_hook"]);
|
|
2111
|
+
function getPublicCommands(program2) {
|
|
2112
|
+
return program2.commands.filter((command) => !HIDDEN_COMMANDS.has(command.name())).map((command) => ({ name: command.name(), description: command.description() }));
|
|
2113
|
+
}
|
|
2114
|
+
function registerCompletions(program2) {
|
|
2115
|
+
program2.command("completions [shell]").description("Print shell completion script (bash, zsh, fish)").action((shell) => {
|
|
2116
|
+
const detected = shell ?? (process.env.SHELL ?? "").split("/").at(-1);
|
|
2117
|
+
if (!detected || !["bash", "zsh", "fish"].includes(detected)) {
|
|
2118
|
+
log.error(`Unknown shell: "${detected ?? ""}". Supported: bash, zsh, fish`);
|
|
2119
|
+
log.plain(` Usage: workscribe completions <bash|zsh|fish>`);
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
const commands = getPublicCommands(program2);
|
|
2123
|
+
const script = detected === "bash" ? bashCompletion(commands) : detected === "zsh" ? zshCompletion(commands) : fishCompletion(commands);
|
|
2124
|
+
process.stdout.write(script + "\n");
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1659
2128
|
// src/cli/init.ts
|
|
1660
2129
|
function detectShell() {
|
|
1661
2130
|
const shell = process.env.SHELL ?? "";
|
|
1662
|
-
if (shell.includes("zsh")) return { name: "zsh", rcFile:
|
|
1663
|
-
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") };
|
|
2133
|
+
if (shell.includes("fish")) {
|
|
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") };
|
|
2136
|
+
}
|
|
1664
2137
|
return null;
|
|
1665
2138
|
}
|
|
1666
2139
|
function step(current, total, label) {
|
|
@@ -1671,19 +2144,21 @@ function registerInit(program2) {
|
|
|
1671
2144
|
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
1672
2145
|
const TOTAL_STEPS = 5;
|
|
1673
2146
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1674
|
-
step(1, TOTAL_STEPS, "Creating data
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2147
|
+
step(1, TOTAL_STEPS, "Creating data directories");
|
|
2148
|
+
for (const dir of REQUIRED_DIRS) {
|
|
2149
|
+
if (!import_fs14.default.existsSync(dir)) {
|
|
2150
|
+
import_fs14.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2151
|
+
log.success(`Created ${dir}`);
|
|
2152
|
+
} else {
|
|
2153
|
+
try {
|
|
2154
|
+
import_fs14.default.chmodSync(dir, 448);
|
|
2155
|
+
} catch {
|
|
2156
|
+
}
|
|
2157
|
+
log.info(`${dir} already exists`);
|
|
1682
2158
|
}
|
|
1683
|
-
log.info(`${WORKSCRIBE_DIR} already exists`);
|
|
1684
2159
|
}
|
|
1685
|
-
if (!
|
|
1686
|
-
|
|
2160
|
+
if (!import_fs14.default.existsSync(PROFILES_DIR)) {
|
|
2161
|
+
import_fs14.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1687
2162
|
}
|
|
1688
2163
|
step(2, TOTAL_STEPS, "Initialising database");
|
|
1689
2164
|
getDb();
|
|
@@ -1691,15 +2166,23 @@ function registerInit(program2) {
|
|
|
1691
2166
|
step(3, TOTAL_STEPS, "Installing shell hook");
|
|
1692
2167
|
const shell = detectShell();
|
|
1693
2168
|
if (!shell) {
|
|
1694
|
-
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
1695
|
-
log.plain(
|
|
2169
|
+
log.warn("Could not detect shell. Supported: zsh, bash, fish");
|
|
2170
|
+
log.plain(
|
|
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")}`
|
|
2172
|
+
);
|
|
1696
2173
|
} else {
|
|
1697
|
-
const hookFile =
|
|
1698
|
-
if (!
|
|
2174
|
+
const hookFile = import_path10.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
2175
|
+
if (!import_fs14.default.existsSync(hookFile)) {
|
|
1699
2176
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
1700
2177
|
} else {
|
|
1701
|
-
const hookContent =
|
|
1702
|
-
|
|
2178
|
+
const hookContent = import_fs14.default.readFileSync(hookFile, "utf-8");
|
|
2179
|
+
const binaryPath = resolveWorkscribeBinary();
|
|
2180
|
+
const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath) : hookContent;
|
|
2181
|
+
if (shell.name === "fish") {
|
|
2182
|
+
const confDir = import_path10.default.dirname(shell.rcFile);
|
|
2183
|
+
if (!import_fs14.default.existsSync(confDir)) import_fs14.default.mkdirSync(confDir, { recursive: true });
|
|
2184
|
+
}
|
|
2185
|
+
if (!import_fs14.default.existsSync(shell.rcFile)) import_fs14.default.writeFileSync(shell.rcFile, "");
|
|
1703
2186
|
if (!isHookInstalled(shell.rcFile)) {
|
|
1704
2187
|
log.plain("");
|
|
1705
2188
|
log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
|
|
@@ -1707,9 +2190,9 @@ function registerInit(program2) {
|
|
|
1707
2190
|
log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
|
|
1708
2191
|
log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
|
|
1709
2192
|
log.plain("");
|
|
1710
|
-
installHook(shell.rcFile,
|
|
2193
|
+
installHook(shell.rcFile, hookWithPath);
|
|
1711
2194
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
1712
|
-
} else if (isHookOutdated(shell.rcFile,
|
|
2195
|
+
} else if (isHookOutdated(shell.rcFile, hookWithPath)) {
|
|
1713
2196
|
const { update } = await (0, import_prompts.default)({
|
|
1714
2197
|
type: "confirm",
|
|
1715
2198
|
name: "update",
|
|
@@ -1717,7 +2200,7 @@ function registerInit(program2) {
|
|
|
1717
2200
|
initial: true
|
|
1718
2201
|
});
|
|
1719
2202
|
if (update) {
|
|
1720
|
-
updateHook(shell.rcFile,
|
|
2203
|
+
updateHook(shell.rcFile, hookWithPath);
|
|
1721
2204
|
log.success(`Shell hook updated in ${shell.rcFile}`);
|
|
1722
2205
|
} else {
|
|
1723
2206
|
log.info("Shell hook update skipped");
|
|
@@ -1725,6 +2208,24 @@ function registerInit(program2) {
|
|
|
1725
2208
|
} else {
|
|
1726
2209
|
log.info(`Shell hook up to date in ${shell.rcFile}`);
|
|
1727
2210
|
}
|
|
2211
|
+
if (isCompletionsInstalled(shell)) {
|
|
2212
|
+
log.info("Shell completions already installed");
|
|
2213
|
+
} else {
|
|
2214
|
+
const { installComps } = await (0, import_prompts.default)({
|
|
2215
|
+
type: "confirm",
|
|
2216
|
+
name: "installComps",
|
|
2217
|
+
message: "Install shell completions (tab-complete workscribe commands)?",
|
|
2218
|
+
initial: true
|
|
2219
|
+
});
|
|
2220
|
+
if (installComps) {
|
|
2221
|
+
try {
|
|
2222
|
+
installCompletions(shell, getPublicCommands(program2));
|
|
2223
|
+
log.success("Shell completions installed");
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
log.warn(`Completions install failed: ${error.message}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
1728
2229
|
}
|
|
1729
2230
|
}
|
|
1730
2231
|
step(4, TOTAL_STEPS, "Detecting AI coding tools");
|
|
@@ -1855,7 +2356,7 @@ function registerInit(program2) {
|
|
|
1855
2356
|
}
|
|
1856
2357
|
}
|
|
1857
2358
|
}
|
|
1858
|
-
if (!
|
|
2359
|
+
if (!import_fs14.default.existsSync(CONFIG_PATH)) {
|
|
1859
2360
|
writeConfig(config);
|
|
1860
2361
|
}
|
|
1861
2362
|
log.plain("");
|
|
@@ -1863,8 +2364,13 @@ function registerInit(program2) {
|
|
|
1863
2364
|
log.plain("");
|
|
1864
2365
|
if (shell) {
|
|
1865
2366
|
log.plain(` Activate capture now:`);
|
|
1866
|
-
|
|
1867
|
-
|
|
2367
|
+
if (shell.name === "fish") {
|
|
2368
|
+
log.plain(` Restart your terminal`);
|
|
2369
|
+
log.plain(` ${import_chalk2.default.dim(`or run: source ${shell.rcFile}`)}`);
|
|
2370
|
+
} else {
|
|
2371
|
+
log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
|
|
2372
|
+
log.plain(` Or restart your terminal.`);
|
|
2373
|
+
}
|
|
1868
2374
|
log.plain("");
|
|
1869
2375
|
}
|
|
1870
2376
|
if (providerConfigured) {
|
|
@@ -1886,7 +2392,7 @@ function registerInit(program2) {
|
|
|
1886
2392
|
}
|
|
1887
2393
|
|
|
1888
2394
|
// src/cli/status.ts
|
|
1889
|
-
var
|
|
2395
|
+
var import_fs15 = __toESM(require("fs"));
|
|
1890
2396
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1891
2397
|
function registerStatus(program2) {
|
|
1892
2398
|
program2.command("status").description("Show capture status and event counts").action(() => {
|
|
@@ -1894,7 +2400,7 @@ function registerStatus(program2) {
|
|
|
1894
2400
|
const total = getEventCount();
|
|
1895
2401
|
const todayEvents = getEventsForDate(todayDate());
|
|
1896
2402
|
const last = getLastEvent();
|
|
1897
|
-
const isPaused =
|
|
2403
|
+
const isPaused = import_fs15.default.existsSync(PAUSE_FLAG_PATH);
|
|
1898
2404
|
log.plain("");
|
|
1899
2405
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
1900
2406
|
log.plain("");
|
|
@@ -1917,6 +2423,13 @@ function registerStatus(program2) {
|
|
|
1917
2423
|
log.plain(
|
|
1918
2424
|
` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
|
|
1919
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
|
+
}
|
|
1920
2433
|
const { total: weekTotal, otherCount } = getRecentOtherRate(7);
|
|
1921
2434
|
if (weekTotal > 0) {
|
|
1922
2435
|
const otherPct = Math.round(otherCount / weekTotal * 100);
|
|
@@ -2010,13 +2523,16 @@ function registerEvents(program2) {
|
|
|
2010
2523
|
var import_chalk5 = __toESM(require("chalk"));
|
|
2011
2524
|
|
|
2012
2525
|
// src/sessions/grouper.ts
|
|
2526
|
+
var LONG_RUNNING_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2013
2527
|
function splitByIdleGap(events, idleMs) {
|
|
2014
2528
|
if (events.length === 0) return [];
|
|
2015
2529
|
const groups = [[events[0]]];
|
|
2016
2530
|
for (let i = 1; i < events.length; i++) {
|
|
2017
2531
|
const prev = new Date(events[i - 1].captured_at).getTime();
|
|
2018
|
-
const
|
|
2019
|
-
|
|
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) {
|
|
2020
2536
|
groups.push([events[i]]);
|
|
2021
2537
|
} else {
|
|
2022
2538
|
groups[groups.length - 1].push(events[i]);
|
|
@@ -2024,13 +2540,17 @@ function splitByIdleGap(events, idleMs) {
|
|
|
2024
2540
|
}
|
|
2025
2541
|
return groups;
|
|
2026
2542
|
}
|
|
2027
|
-
|
|
2543
|
+
var REPO_SWITCH_GAP_MS = 10 * 60 * 1e3;
|
|
2544
|
+
function splitByRepo(events, gapMs = 0) {
|
|
2028
2545
|
if (events.length === 0) return [];
|
|
2029
2546
|
const groups = [[events[0]]];
|
|
2030
2547
|
for (let i = 1; i < events.length; i++) {
|
|
2031
2548
|
const prevRepo = events[i - 1].repo_name;
|
|
2032
2549
|
const currRepo = events[i].repo_name;
|
|
2033
|
-
|
|
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) {
|
|
2034
2554
|
groups.push([events[i]]);
|
|
2035
2555
|
} else {
|
|
2036
2556
|
groups[groups.length - 1].push(events[i]);
|
|
@@ -2075,7 +2595,7 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
|
|
|
2075
2595
|
const timeGroups = splitByIdleGap(events, idleMs);
|
|
2076
2596
|
for (const group of timeGroups) {
|
|
2077
2597
|
if (group.length === 0) continue;
|
|
2078
|
-
const repoGroups = splitByRepo(group);
|
|
2598
|
+
const repoGroups = splitByRepo(group, REPO_SWITCH_GAP_MS);
|
|
2079
2599
|
for (const repoGroup of repoGroups) {
|
|
2080
2600
|
if (repoGroup.length === 0) continue;
|
|
2081
2601
|
const first = repoGroup[0];
|
|
@@ -2154,9 +2674,9 @@ function registerSessions(program2) {
|
|
|
2154
2674
|
|
|
2155
2675
|
// src/cli/summary.ts
|
|
2156
2676
|
var import_chalk6 = __toESM(require("chalk"));
|
|
2157
|
-
var
|
|
2158
|
-
var
|
|
2159
|
-
var
|
|
2677
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2678
|
+
var import_os6 = __toESM(require("os"));
|
|
2679
|
+
var import_path11 = __toESM(require("path"));
|
|
2160
2680
|
|
|
2161
2681
|
// src/integrations/webhook.ts
|
|
2162
2682
|
async function postToWebhook(url, text) {
|
|
@@ -2224,7 +2744,7 @@ async function handleWebhook(opts, config, summary) {
|
|
|
2224
2744
|
if (webhookUrl) {
|
|
2225
2745
|
try {
|
|
2226
2746
|
log.info("Posting to webhook\u2026");
|
|
2227
|
-
const username =
|
|
2747
|
+
const username = import_os6.default.userInfo().username;
|
|
2228
2748
|
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
2229
2749
|
await postToWebhook(webhookUrl, body);
|
|
2230
2750
|
log.success("Posted.");
|
|
@@ -2261,9 +2781,9 @@ function registerSummary(program2) {
|
|
|
2261
2781
|
return;
|
|
2262
2782
|
}
|
|
2263
2783
|
const cacheKey = isoWeekKey(today);
|
|
2264
|
-
const cacheFile =
|
|
2265
|
-
if (!opts.fresh &&
|
|
2266
|
-
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");
|
|
2267
2787
|
log.plain("");
|
|
2268
2788
|
log.plain(cached);
|
|
2269
2789
|
log.plain("");
|
|
@@ -2279,8 +2799,8 @@ function registerSummary(program2) {
|
|
|
2279
2799
|
log.plain("");
|
|
2280
2800
|
log.plain(summary);
|
|
2281
2801
|
log.plain("");
|
|
2282
|
-
|
|
2283
|
-
|
|
2802
|
+
import_fs16.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2803
|
+
import_fs16.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
2284
2804
|
await handleWebhook(opts, config, summary);
|
|
2285
2805
|
} catch (error) {
|
|
2286
2806
|
log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
|
|
@@ -2398,8 +2918,8 @@ ${bullets.trim()}`;
|
|
|
2398
2918
|
}
|
|
2399
2919
|
|
|
2400
2920
|
// src/cli/export.ts
|
|
2401
|
-
var
|
|
2402
|
-
var
|
|
2921
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2922
|
+
var import_path12 = __toESM(require("path"));
|
|
2403
2923
|
function eventRowToPayload2(row) {
|
|
2404
2924
|
let metadata = {};
|
|
2405
2925
|
try {
|
|
@@ -2455,9 +2975,9 @@ function registerExport(program2) {
|
|
|
2455
2975
|
const payload = { date, sessions: sessionPayloads };
|
|
2456
2976
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2457
2977
|
const summary = await provider.summarize(payload);
|
|
2458
|
-
const outputPath = opts.output ??
|
|
2459
|
-
if (!
|
|
2460
|
-
|
|
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}
|
|
2461
2981
|
|
|
2462
2982
|
${summary}
|
|
2463
2983
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2529,22 +3049,22 @@ function printConfig(obj, indent) {
|
|
|
2529
3049
|
}
|
|
2530
3050
|
|
|
2531
3051
|
// src/cli/pause.ts
|
|
2532
|
-
var
|
|
3052
|
+
var import_fs18 = __toESM(require("fs"));
|
|
2533
3053
|
function registerPause(program2) {
|
|
2534
3054
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2535
|
-
if (
|
|
3055
|
+
if (import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2536
3056
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2537
3057
|
return;
|
|
2538
3058
|
}
|
|
2539
|
-
|
|
3059
|
+
import_fs18.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2540
3060
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2541
3061
|
});
|
|
2542
3062
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2543
|
-
if (!
|
|
3063
|
+
if (!import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2544
3064
|
log.info("Capture is not paused.");
|
|
2545
3065
|
return;
|
|
2546
3066
|
}
|
|
2547
|
-
|
|
3067
|
+
import_fs18.default.rmSync(PAUSE_FLAG_PATH);
|
|
2548
3068
|
log.success("Capture resumed.");
|
|
2549
3069
|
});
|
|
2550
3070
|
}
|
|
@@ -2629,18 +3149,19 @@ function registerClear(program2) {
|
|
|
2629
3149
|
}
|
|
2630
3150
|
|
|
2631
3151
|
// src/cli/uninstall.ts
|
|
2632
|
-
var
|
|
2633
|
-
var
|
|
2634
|
-
var
|
|
3152
|
+
var import_fs19 = __toESM(require("fs"));
|
|
3153
|
+
var import_path13 = __toESM(require("path"));
|
|
3154
|
+
var import_os7 = __toESM(require("os"));
|
|
2635
3155
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2636
3156
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2637
3157
|
function detectShellRcFiles() {
|
|
2638
3158
|
const candidates = [
|
|
2639
|
-
{ name: "zsh", rcFile:
|
|
2640
|
-
{ name: "bash", rcFile:
|
|
2641
|
-
{ name: "bash", 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") }
|
|
2642
3163
|
];
|
|
2643
|
-
return candidates.filter((candidate) =>
|
|
3164
|
+
return candidates.filter((candidate) => import_fs19.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2644
3165
|
}
|
|
2645
3166
|
function registerUninstall(program2) {
|
|
2646
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) => {
|
|
@@ -2650,16 +3171,23 @@ function registerUninstall(program2) {
|
|
|
2650
3171
|
log.info("No shell hooks found");
|
|
2651
3172
|
} else {
|
|
2652
3173
|
for (const { name, rcFile } of hooked) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3174
|
+
if (name === "fish") {
|
|
3175
|
+
import_fs19.default.unlinkSync(rcFile);
|
|
3176
|
+
log.success(`Shell hook removed: ${rcFile}`);
|
|
3177
|
+
log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
|
|
3178
|
+
} else {
|
|
3179
|
+
removeHook(rcFile);
|
|
3180
|
+
log.success(`Shell hook removed from ${rcFile}`);
|
|
3181
|
+
log.plain(` ${import_chalk9.default.dim(`Run: source ${rcFile}`)} (or restart your terminal to take effect)`);
|
|
3182
|
+
log.plain(
|
|
3183
|
+
` ${import_chalk9.default.dim(`${name === "zsh" ? "unfunction" : "unset -f"} _workscribe_preexec _workscribe_precmd`)}`
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
2659
3186
|
}
|
|
2660
3187
|
}
|
|
2661
3188
|
if (opts.hooksOnly) return;
|
|
2662
|
-
|
|
3189
|
+
const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs19.default.existsSync(dir));
|
|
3190
|
+
if (existingDirs.length > 0) {
|
|
2663
3191
|
log.plain("");
|
|
2664
3192
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
2665
3193
|
type: "confirm",
|
|
@@ -2668,8 +3196,10 @@ function registerUninstall(program2) {
|
|
|
2668
3196
|
initial: false
|
|
2669
3197
|
});
|
|
2670
3198
|
if (deleteData) {
|
|
2671
|
-
|
|
2672
|
-
|
|
3199
|
+
for (const dir of existingDirs) {
|
|
3200
|
+
import_fs19.default.rmSync(dir, { recursive: true, force: true });
|
|
3201
|
+
log.success(`Deleted ${dir}`);
|
|
3202
|
+
}
|
|
2673
3203
|
} else {
|
|
2674
3204
|
log.info(`Data kept at ${WORKSCRIBE_DIR}`);
|
|
2675
3205
|
}
|
|
@@ -2681,8 +3211,8 @@ function registerUninstall(program2) {
|
|
|
2681
3211
|
}
|
|
2682
3212
|
|
|
2683
3213
|
// src/cli/profile.ts
|
|
2684
|
-
var
|
|
2685
|
-
var
|
|
3214
|
+
var import_fs20 = __toESM(require("fs"));
|
|
3215
|
+
var import_path14 = __toESM(require("path"));
|
|
2686
3216
|
var import_chalk10 = __toESM(require("chalk"));
|
|
2687
3217
|
var MIN_SUGGEST_COUNT = 5;
|
|
2688
3218
|
function registerProfile(program2) {
|
|
@@ -2728,7 +3258,7 @@ function registerProfile(program2) {
|
|
|
2728
3258
|
log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
|
|
2729
3259
|
for (const entry of entries) {
|
|
2730
3260
|
const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
|
|
2731
|
-
const installed =
|
|
3261
|
+
const installed = import_fs20.default.existsSync(import_path14.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
|
|
2732
3262
|
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
|
|
2733
3263
|
if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
|
|
2734
3264
|
}
|
|
@@ -2764,8 +3294,8 @@ function registerProfile(program2) {
|
|
|
2764
3294
|
continue;
|
|
2765
3295
|
}
|
|
2766
3296
|
activeNames.delete(name);
|
|
2767
|
-
const cachePath =
|
|
2768
|
-
if (
|
|
3297
|
+
const cachePath = import_path14.default.join(PROFILES_DIR, `${name}.json`);
|
|
3298
|
+
if (import_fs20.default.existsSync(cachePath)) import_fs20.default.unlinkSync(cachePath);
|
|
2769
3299
|
log.success(`Profile "${name}" removed`);
|
|
2770
3300
|
}
|
|
2771
3301
|
config.capture.profiles = [...activeNames];
|
|
@@ -2829,14 +3359,37 @@ function registerProfile(program2) {
|
|
|
2829
3359
|
const suggestions = [];
|
|
2830
3360
|
for (const entry of manifest) {
|
|
2831
3361
|
if (activeNames.has(entry.name)) continue;
|
|
2832
|
-
const matchedCount = toolCounts.filter(
|
|
3362
|
+
const matchedCount = toolCounts.filter(
|
|
3363
|
+
(toolCount) => entry.keywords.some((keyword) => toolCount.tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3364
|
+
).reduce((total, toolCount) => total + toolCount.count, 0);
|
|
2833
3365
|
if (matchedCount >= MIN_SUGGEST_COUNT) {
|
|
2834
3366
|
suggestions.push({ entry, count: matchedCount });
|
|
2835
3367
|
}
|
|
2836
3368
|
}
|
|
2837
3369
|
suggestions.sort((a, b) => b.count - a.count);
|
|
2838
3370
|
if (suggestions.length === 0) {
|
|
2839
|
-
|
|
3371
|
+
const activeEntries = manifest.filter((entry) => activeNames.has(entry.name));
|
|
3372
|
+
const isToolCoveredByActiveProfile = (tool) => activeEntries.some(
|
|
3373
|
+
(entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3374
|
+
);
|
|
3375
|
+
const uncoveredTools = toolCounts.filter((toolCount) => !isToolCoveredByActiveProfile(toolCount.tool));
|
|
3376
|
+
if (uncoveredTools.length === 0) {
|
|
3377
|
+
log.plain("");
|
|
3378
|
+
log.plain("Your active profiles cover all unclassified commands.");
|
|
3379
|
+
log.plain(import_chalk10.default.dim(" Future captures will be classified correctly."));
|
|
3380
|
+
log.plain("");
|
|
3381
|
+
} else {
|
|
3382
|
+
log.plain("");
|
|
3383
|
+
log.plain("No profile matches found for your unclassified commands.");
|
|
3384
|
+
log.plain("");
|
|
3385
|
+
log.plain(import_chalk10.default.dim(" Top unclassified tools:"));
|
|
3386
|
+
for (const { tool, count } of uncoveredTools.slice(0, 5)) {
|
|
3387
|
+
log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
|
|
3388
|
+
}
|
|
3389
|
+
log.plain("");
|
|
3390
|
+
log.plain(import_chalk10.default.dim(" Run workscribe events --category other to review them."));
|
|
3391
|
+
log.plain("");
|
|
3392
|
+
}
|
|
2840
3393
|
return;
|
|
2841
3394
|
}
|
|
2842
3395
|
log.plain("");
|
|
@@ -2848,8 +3401,25 @@ function registerProfile(program2) {
|
|
|
2848
3401
|
);
|
|
2849
3402
|
}
|
|
2850
3403
|
log.plain("");
|
|
2851
|
-
log.plain(
|
|
3404
|
+
log.plain(
|
|
3405
|
+
` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((suggestion) => suggestion.entry.name).join(" "))}`
|
|
3406
|
+
);
|
|
2852
3407
|
log.plain("");
|
|
3408
|
+
const allCoveredEntries = [
|
|
3409
|
+
...manifest.filter((entry) => activeNames.has(entry.name)),
|
|
3410
|
+
...suggestions.map((suggestion) => suggestion.entry)
|
|
3411
|
+
];
|
|
3412
|
+
const isToolCoveredAfterInstall = (tool) => allCoveredEntries.some(
|
|
3413
|
+
(entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3414
|
+
);
|
|
3415
|
+
const stillUncovered = toolCounts.filter((toolCount) => !isToolCoveredAfterInstall(toolCount.tool)).slice(0, 5);
|
|
3416
|
+
if (stillUncovered.length > 0) {
|
|
3417
|
+
log.plain(import_chalk10.default.dim(" Still uncovered after installing:"));
|
|
3418
|
+
for (const { tool, count } of stillUncovered) {
|
|
3419
|
+
log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
|
|
3420
|
+
}
|
|
3421
|
+
log.plain("");
|
|
3422
|
+
}
|
|
2853
3423
|
});
|
|
2854
3424
|
}
|
|
2855
3425
|
|
|
@@ -2866,7 +3436,9 @@ function registerNote(program2) {
|
|
|
2866
3436
|
repo_name: repoName,
|
|
2867
3437
|
branch,
|
|
2868
3438
|
category: "note",
|
|
2869
|
-
metadata: JSON.stringify({ text })
|
|
3439
|
+
metadata: JSON.stringify({ text }),
|
|
3440
|
+
source: null,
|
|
3441
|
+
file_path: null
|
|
2870
3442
|
});
|
|
2871
3443
|
log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
|
|
2872
3444
|
});
|
|
@@ -2874,9 +3446,9 @@ function registerNote(program2) {
|
|
|
2874
3446
|
|
|
2875
3447
|
// src/cli/week.ts
|
|
2876
3448
|
var import_chalk11 = __toESM(require("chalk"));
|
|
2877
|
-
var
|
|
2878
|
-
var
|
|
2879
|
-
var
|
|
3449
|
+
var import_fs21 = __toESM(require("fs"));
|
|
3450
|
+
var import_path15 = __toESM(require("path"));
|
|
3451
|
+
var import_os8 = __toESM(require("os"));
|
|
2880
3452
|
function eventRowToPayload3(row) {
|
|
2881
3453
|
let metadata = {};
|
|
2882
3454
|
try {
|
|
@@ -2915,7 +3487,7 @@ async function postSummary(opts, config, summary) {
|
|
|
2915
3487
|
}
|
|
2916
3488
|
try {
|
|
2917
3489
|
log.info("Posting to webhook\u2026");
|
|
2918
|
-
const username =
|
|
3490
|
+
const username = import_os8.default.userInfo().username;
|
|
2919
3491
|
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
2920
3492
|
await postToWebhook(webhookUrl, body);
|
|
2921
3493
|
log.success("Posted.");
|
|
@@ -2948,9 +3520,9 @@ function registerWeek(program2) {
|
|
|
2948
3520
|
return;
|
|
2949
3521
|
}
|
|
2950
3522
|
const cacheKey = isoWeekKey(today);
|
|
2951
|
-
const cacheFile =
|
|
2952
|
-
if (!opts.fresh &&
|
|
2953
|
-
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");
|
|
2954
3526
|
log.plain("");
|
|
2955
3527
|
log.plain(cached);
|
|
2956
3528
|
log.plain("");
|
|
@@ -2966,8 +3538,8 @@ function registerWeek(program2) {
|
|
|
2966
3538
|
log.plain("");
|
|
2967
3539
|
log.plain(summary);
|
|
2968
3540
|
log.plain("");
|
|
2969
|
-
|
|
2970
|
-
|
|
3541
|
+
import_fs21.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
3542
|
+
import_fs21.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
2971
3543
|
await postSummary(opts, config, summary);
|
|
2972
3544
|
} catch (error) {
|
|
2973
3545
|
log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
|
|
@@ -2976,10 +3548,236 @@ function registerWeek(program2) {
|
|
|
2976
3548
|
});
|
|
2977
3549
|
}
|
|
2978
3550
|
|
|
3551
|
+
// src/cli/bug-report.ts
|
|
3552
|
+
var import_fs22 = __toESM(require("fs"));
|
|
3553
|
+
var import_os9 = __toESM(require("os"));
|
|
3554
|
+
var import_path16 = __toESM(require("path"));
|
|
3555
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
3556
|
+
var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
|
|
3557
|
+
var DEBUG_LOG_TAIL_LINES = 100;
|
|
3558
|
+
function tailFile(filePath, lines) {
|
|
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");
|
|
3561
|
+
const allLines = content.split("\n");
|
|
3562
|
+
return allLines.slice(-lines).join("\n");
|
|
3563
|
+
}
|
|
3564
|
+
function redactConfig() {
|
|
3565
|
+
try {
|
|
3566
|
+
const config = readConfig();
|
|
3567
|
+
if (config.ai.apiKey) config.ai.apiKey = "<redacted>";
|
|
3568
|
+
if (config.integrations?.slackWebhook) config.integrations.slackWebhook = "<redacted>";
|
|
3569
|
+
if (config.integrations?.webhook) config.integrations.webhook = "<redacted>";
|
|
3570
|
+
return JSON.stringify(config, null, 2);
|
|
3571
|
+
} catch {
|
|
3572
|
+
return "(could not read config)";
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
function registerBugReport(program2) {
|
|
3576
|
+
program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
|
|
3577
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3578
|
+
const reportPath = import_path16.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
|
|
3579
|
+
const systemInfo = [
|
|
3580
|
+
`Workscribe version : ${"1.13.0"}`,
|
|
3581
|
+
`Node.js version : ${process.version}`,
|
|
3582
|
+
`Platform : ${process.platform}`,
|
|
3583
|
+
`Architecture : ${process.arch}`,
|
|
3584
|
+
`OS release : ${import_os9.default.release()}`,
|
|
3585
|
+
`Config path : ${CONFIG_PATH}`,
|
|
3586
|
+
`Data directory : ${WORKSCRIBE_DIR}`
|
|
3587
|
+
].join("\n");
|
|
3588
|
+
const report = [
|
|
3589
|
+
"=== Workscribe Bug Report ===",
|
|
3590
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
3591
|
+
"",
|
|
3592
|
+
"--- System ---",
|
|
3593
|
+
systemInfo,
|
|
3594
|
+
"",
|
|
3595
|
+
"--- Config (secrets redacted) ---",
|
|
3596
|
+
redactConfig(),
|
|
3597
|
+
"",
|
|
3598
|
+
`--- Debug log (last ${DEBUG_LOG_TAIL_LINES} lines) ---`,
|
|
3599
|
+
tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
|
|
3600
|
+
].join("\n");
|
|
3601
|
+
try {
|
|
3602
|
+
if (!import_fs22.default.existsSync(WORKSCRIBE_DIR)) {
|
|
3603
|
+
import_fs22.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
3604
|
+
}
|
|
3605
|
+
import_fs22.default.writeFileSync(reportPath, report, { mode: 384 });
|
|
3606
|
+
} catch (error) {
|
|
3607
|
+
log.error(`Could not write report: ${error.message}`);
|
|
3608
|
+
process.exit(1);
|
|
3609
|
+
}
|
|
3610
|
+
const params = new URLSearchParams({
|
|
3611
|
+
title: `Bug report \u2014 workscribe ${"1.13.0"}`,
|
|
3612
|
+
body: `<!-- Attach ${import_path16.default.basename(reportPath)} or paste the relevant sections below -->
|
|
3613
|
+
|
|
3614
|
+
**Describe the issue:**
|
|
3615
|
+
|
|
3616
|
+
**Steps to reproduce:**
|
|
3617
|
+
|
|
3618
|
+
**Expected behaviour:**`
|
|
3619
|
+
});
|
|
3620
|
+
log.success(`Report written to: ${import_chalk12.default.cyan(reportPath)}`);
|
|
3621
|
+
log.plain("");
|
|
3622
|
+
log.plain(` Open a GitHub issue:`);
|
|
3623
|
+
log.plain(` ${import_chalk12.default.cyan(`${GITHUB_ISSUES_URL}?${params}`)}`);
|
|
3624
|
+
log.plain("");
|
|
3625
|
+
log.plain(import_chalk12.default.dim(` Attach the report or paste the relevant sections into the issue.`));
|
|
3626
|
+
log.plain("");
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
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
|
+
|
|
3730
|
+
// src/utils/update-check.ts
|
|
3731
|
+
var import_fs23 = __toESM(require("fs"));
|
|
3732
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3733
|
+
function readCache() {
|
|
3734
|
+
try {
|
|
3735
|
+
return JSON.parse(import_fs23.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
|
|
3736
|
+
} catch {
|
|
3737
|
+
return null;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
function writeCache(cache) {
|
|
3741
|
+
try {
|
|
3742
|
+
import_fs23.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
|
|
3743
|
+
} catch {
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
function isNewer(latest, current) {
|
|
3747
|
+
const parse = (version) => version.replace(/^v/, "").split(".").map(Number);
|
|
3748
|
+
const [latestMajor, latestMinor, latestPatch] = parse(latest);
|
|
3749
|
+
const [currentMajor, currentMinor, currentPatch] = parse(current);
|
|
3750
|
+
if (latestMajor !== currentMajor) return latestMajor > currentMajor;
|
|
3751
|
+
if (latestMinor !== currentMinor) return latestMinor > currentMinor;
|
|
3752
|
+
return latestPatch > currentPatch;
|
|
3753
|
+
}
|
|
3754
|
+
function checkForUpdate() {
|
|
3755
|
+
const current = "1.13.0";
|
|
3756
|
+
const cache = readCache();
|
|
3757
|
+
if (cache && isNewer(cache.latestVersion, current)) {
|
|
3758
|
+
process.stderr.write(
|
|
3759
|
+
`
|
|
3760
|
+
Update available ${current} \u2192 ${cache.latestVersion}
|
|
3761
|
+
Run: npm install -g @workscribe/cli
|
|
3762
|
+
|
|
3763
|
+
`
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3766
|
+
const now = Date.now();
|
|
3767
|
+
if (!cache || now - cache.lastChecked > CHECK_INTERVAL_MS) {
|
|
3768
|
+
fetch("https://registry.npmjs.org/@workscribe/cli/latest").then((response) => response.json()).then((data) => {
|
|
3769
|
+
const latest = data.version;
|
|
3770
|
+
if (latest) writeCache({ lastChecked: now, latestVersion: latest });
|
|
3771
|
+
}).catch(() => {
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
|
|
2979
3776
|
// src/index.ts
|
|
2980
3777
|
assertNodeVersion(process.versions.node);
|
|
3778
|
+
checkForUpdate();
|
|
2981
3779
|
var program = new import_commander.Command();
|
|
2982
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
3780
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.13.0");
|
|
2983
3781
|
registerCapture(program);
|
|
2984
3782
|
registerHook(program);
|
|
2985
3783
|
registerInit(program);
|
|
@@ -2996,5 +3794,9 @@ registerUninstall(program);
|
|
|
2996
3794
|
registerProfile(program);
|
|
2997
3795
|
registerNote(program);
|
|
2998
3796
|
registerWeek(program);
|
|
3797
|
+
registerBugReport(program);
|
|
3798
|
+
registerCompletions(program);
|
|
3799
|
+
registerAlerts(program);
|
|
3800
|
+
registerContext(program);
|
|
2999
3801
|
program.parse();
|
|
3000
3802
|
//# sourceMappingURL=index.js.map
|