codiedev 0.9.82 → 0.9.84
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/cli.d.ts +1 -0
- package/dist/cli.js +36 -13
- package/dist/commands/inbox.js +9 -0
- package/dist/commands/ping.js +11 -7
- package/dist/commands/pingFiles.d.ts +22 -0
- package/dist/commands/pingFiles.js +155 -0
- package/dist/commands/reply.d.ts +5 -0
- package/dist/commands/reply.js +54 -0
- package/dist/hook.js +28 -8
- package/dist/inboxFormat.d.ts +10 -0
- package/dist/inboxFormat.js +18 -1
- package/dist/launcher.d.ts +5 -0
- package/dist/launcher.js +28 -1
- package/dist/mcp.js +1 -48
- package/dist/teamSkillContext.d.ts +1 -1
- package/dist/teamSkillContext.js +1 -1
- package/dist/unreadNotice.d.ts +22 -0
- package/dist/unreadNotice.js +81 -0
- package/dist/utils.js +5 -4
- package/package.json +1 -1
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/ask.js +0 -58
package/dist/cli.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* codiedev ping <user> "<msg>" — send a message (optionally --with <key>)
|
|
10
10
|
* codiedev inbox — list pings (use --unread to filter)
|
|
11
11
|
* codiedev read <ping-id> — mark read + show full message
|
|
12
|
+
* codiedev reply <ping-id> "<msg>" — answer a ping in its thread
|
|
12
13
|
* codiedev note "<text>" — capture a thought
|
|
13
14
|
* codiedev help — show this help
|
|
14
15
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* codiedev ping <user> "<msg>" — send a message (optionally --with <key>)
|
|
11
11
|
* codiedev inbox — list pings (use --unread to filter)
|
|
12
12
|
* codiedev read <ping-id> — mark read + show full message
|
|
13
|
+
* codiedev reply <ping-id> "<msg>" — answer a ping in its thread
|
|
13
14
|
* codiedev note "<text>" — capture a thought
|
|
14
15
|
* codiedev help — show this help
|
|
15
16
|
*/
|
|
@@ -53,8 +54,8 @@ const utils_1 = require("./utils");
|
|
|
53
54
|
const push_1 = require("./commands/push");
|
|
54
55
|
const delete_1 = require("./commands/delete");
|
|
55
56
|
const pull_1 = require("./commands/pull");
|
|
56
|
-
const ask_1 = require("./commands/ask");
|
|
57
57
|
const ping_1 = require("./commands/ping");
|
|
58
|
+
const reply_1 = require("./commands/reply");
|
|
58
59
|
const inbox_1 = require("./commands/inbox");
|
|
59
60
|
const note_1 = require("./commands/note");
|
|
60
61
|
const doctor_1 = require("./commands/doctor");
|
|
@@ -100,19 +101,17 @@ Artifacts:
|
|
|
100
101
|
codiedev delete <key> [-y] Delete every version of an artifact you
|
|
101
102
|
authored (clean up bad/test pushes).
|
|
102
103
|
Asks for confirmation; -y to skip.
|
|
103
|
-
codiedev ask "<question>" Ask a natural-language question of the
|
|
104
|
-
team's collective memory. Returns an
|
|
105
|
-
LLM-synthesized answer with citations
|
|
106
|
-
pointing at sessions + artifacts.
|
|
107
104
|
|
|
108
105
|
Messaging:
|
|
109
|
-
codiedev ping <user> "<msg>" [--with <key>]
|
|
106
|
+
codiedev ping <user> "<msg>" [--with <key>] [--file <path>]
|
|
110
107
|
Send a message to a teammate
|
|
111
108
|
codiedev send <key> <user> ["<msg>"] Share an artifact + ping in one step
|
|
112
109
|
codiedev share <key> <user> [--role read|edit]
|
|
113
110
|
Silently grant access (no notification)
|
|
114
111
|
codiedev inbox [--unread] [--limit N] Show messages
|
|
115
112
|
codiedev read <ping-id> Mark read + show full message
|
|
113
|
+
codiedev reply <ping-id> "<msg>" [--file <path>]
|
|
114
|
+
Answer a message in its thread
|
|
116
115
|
|
|
117
116
|
Starter packs:
|
|
118
117
|
codiedev pack list Packs assigned to your team
|
|
@@ -284,16 +283,36 @@ codiedev pull — fetch an artifact
|
|
|
284
283
|
codiedev ping — send a teammate a message, optionally attached to an artifact
|
|
285
284
|
|
|
286
285
|
Usage:
|
|
287
|
-
codiedev ping <user> "<message>" [--with <artifact-key>]
|
|
286
|
+
codiedev ping <user> "<message>" [--with <artifact-key>] [--file <path>]
|
|
288
287
|
|
|
289
288
|
<user> resolves to a teammate in your org by first name, full name,
|
|
290
289
|
or email. Ambiguous matches return a list — retry with the full email.
|
|
291
290
|
|
|
291
|
+
--file attaches a file (repeat for up to 10, 20 MB each). They see it
|
|
292
|
+
in their inbox and can open it in the portal.
|
|
293
|
+
|
|
292
294
|
Teammates are notified by email and can reply from their own agent.
|
|
295
|
+
To answer a message you received, use 'codiedev reply' instead: it
|
|
296
|
+
keeps the exchange in one thread.
|
|
293
297
|
|
|
294
298
|
Examples:
|
|
295
299
|
codiedev ping <teammate> "thoughts on batch vs parallel?" --with spec-cart-clear.md
|
|
296
300
|
codiedev ping <teammate>@example.com "can you take this one?"
|
|
301
|
+
codiedev ping <teammate> "shard timings from last night" --file ./timings.csv
|
|
302
|
+
`.trim(),
|
|
303
|
+
reply: `
|
|
304
|
+
codiedev reply — answer a message in its thread
|
|
305
|
+
|
|
306
|
+
Usage:
|
|
307
|
+
codiedev reply <ping-id> "<message>" [--file <path>]
|
|
308
|
+
|
|
309
|
+
Grab the ping-id from 'codiedev inbox'. The reply goes to whoever sent
|
|
310
|
+
that ping, stays in the same conversation, and marks their message
|
|
311
|
+
read and answered, so it stops showing as waiting on you.
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
codiedev reply <ping-id> "yes, step 2 covers partial refunds"
|
|
315
|
+
codiedev reply <ping-id> "updated numbers attached" --file ./timings.csv
|
|
297
316
|
`.trim(),
|
|
298
317
|
inbox: `
|
|
299
318
|
codiedev inbox — show messages from teammates
|
|
@@ -415,13 +434,12 @@ signal. The stored artifact type still follows the filename rule
|
|
|
415
434
|
|
|
416
435
|
## Portal
|
|
417
436
|
|
|
418
|
-
|
|
437
|
+
Messages you send and receive are also at:
|
|
419
438
|
|
|
420
439
|
https://codiedev.com/portal/inbox
|
|
421
440
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
email notifications when pinged.
|
|
441
|
+
A teammate can read and answer there without touching a terminal, and
|
|
442
|
+
gets an email when pinged.
|
|
425
443
|
|
|
426
444
|
## Agent-native
|
|
427
445
|
|
|
@@ -564,8 +582,10 @@ async function main() {
|
|
|
564
582
|
await (0, pull_1.runPull)(rest);
|
|
565
583
|
return;
|
|
566
584
|
case "ask":
|
|
567
|
-
|
|
568
|
-
|
|
585
|
+
// Removed 2026-11. Kept as a named case so the answer is a pointer,
|
|
586
|
+
// not "unknown command".
|
|
587
|
+
console.error('codiedev ask has been removed. Use: codiedev search "<query>"');
|
|
588
|
+
process.exit(1);
|
|
569
589
|
case "ping":
|
|
570
590
|
await (0, ping_1.runPing)(rest);
|
|
571
591
|
return;
|
|
@@ -582,6 +602,9 @@ async function main() {
|
|
|
582
602
|
case "read":
|
|
583
603
|
await (0, inbox_1.runRead)(rest);
|
|
584
604
|
return;
|
|
605
|
+
case "reply":
|
|
606
|
+
await (0, reply_1.runReply)(rest);
|
|
607
|
+
return;
|
|
585
608
|
case "scrub": {
|
|
586
609
|
const { runScrubCommand } = await Promise.resolve().then(() => __importStar(require("./commands/scrub")));
|
|
587
610
|
process.exitCode = await runScrubCommand(rest);
|
package/dist/commands/inbox.js
CHANGED
|
@@ -107,7 +107,16 @@ async function runRead(args) {
|
|
|
107
107
|
console.log();
|
|
108
108
|
console.log(ping.body || "(empty)");
|
|
109
109
|
console.log();
|
|
110
|
+
if (ping.attachments?.length) {
|
|
111
|
+
console.log(ping.attachments.length === 1 ? "File:" : "Files:");
|
|
112
|
+
for (const a of ping.attachments) {
|
|
113
|
+
console.log(` ${(0, inboxFormat_1.fileList)([a])}`);
|
|
114
|
+
console.log(` ${a.url ?? "(no longer stored)"}`);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
110
118
|
console.log("✓ Marked as read");
|
|
119
|
+
console.log(`Reply: codiedev reply ${ping.pingId} "<message>"`);
|
|
111
120
|
if (ping.subjectKey) {
|
|
112
121
|
console.log(`Pull: codiedev pull ${ping.subjectKey}`);
|
|
113
122
|
}
|
package/dist/commands/ping.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runPing = runPing;
|
|
4
4
|
const shared_1 = require("./shared");
|
|
5
|
-
|
|
5
|
+
const pingFiles_1 = require("./pingFiles");
|
|
6
|
+
function parseArgs(allArgs) {
|
|
7
|
+
const { files, rest: args } = (0, pingFiles_1.takeFileArgs)(allArgs);
|
|
6
8
|
let to;
|
|
7
9
|
const positional = [];
|
|
8
10
|
let subjectKey;
|
|
@@ -18,31 +20,33 @@ function parseArgs(args) {
|
|
|
18
20
|
positional.push(a);
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
// A file can stand in for the message.
|
|
24
|
+
if (positional.length < 2 && !(positional.length === 1 && files.length > 0)) {
|
|
25
|
+
console.error("Usage: codiedev ping <user> \"<message>\" [--with <artifact-key>] [--file <path>]");
|
|
23
26
|
process.exit(1);
|
|
24
27
|
}
|
|
25
28
|
to = positional[0];
|
|
26
29
|
// Join remaining positionals in case the shell split the message on spaces
|
|
27
30
|
// despite quoting (happens with some escape sequences).
|
|
28
31
|
const body = (0, shared_1.stripQuotes)(positional.slice(1).join(" "));
|
|
29
|
-
return { to, body, subjectKey };
|
|
32
|
+
return { to, body: body || (files.length ? "Sent a file." : ""), subjectKey, files };
|
|
30
33
|
}
|
|
31
34
|
async function runPing(args) {
|
|
32
|
-
const { to, body, subjectKey } = parseArgs(args);
|
|
35
|
+
const { to, body, subjectKey, files } = parseArgs(args);
|
|
33
36
|
const config = (0, shared_1.requireConfig)();
|
|
34
37
|
const start = Date.now();
|
|
35
38
|
try {
|
|
39
|
+
const attachmentIds = await (0, pingFiles_1.uploadOrExit)(config, files);
|
|
36
40
|
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ping", {
|
|
37
41
|
config,
|
|
38
|
-
body: { to, body, subjectKey },
|
|
42
|
+
body: { to, body, subjectKey, ...(attachmentIds.length ? { attachmentIds } : {}) },
|
|
39
43
|
});
|
|
40
44
|
(0, shared_1.recordAgentEvent)(config, {
|
|
41
45
|
tool: "codiedev_ping",
|
|
42
46
|
ok: true,
|
|
43
47
|
latencyMs: Date.now() - start,
|
|
44
48
|
});
|
|
45
|
-
console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}`);
|
|
49
|
+
console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}${files.length ? ` with ${files.length} file${files.length === 1 ? "" : "s"}` : ""}`);
|
|
46
50
|
}
|
|
47
51
|
catch (err) {
|
|
48
52
|
const e = err;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CodiedevConfig } from "../utils";
|
|
2
|
+
export declare const MAX_PING_FILES = 10;
|
|
3
|
+
export declare const MAX_PING_FILE_BYTES: number;
|
|
4
|
+
export declare function contentTypeFor(fileName: string): string;
|
|
5
|
+
export declare function formatBytes(bytes: number): string;
|
|
6
|
+
/** Pull every `--file <path>` / `-f <path>` / `--file=<path>` out of argv,
|
|
7
|
+
* returning the paths and the remaining args untouched. */
|
|
8
|
+
export declare function takeFileArgs(args: string[]): {
|
|
9
|
+
files: string[];
|
|
10
|
+
rest: string[];
|
|
11
|
+
};
|
|
12
|
+
export type FileProblem = {
|
|
13
|
+
path: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
/** Check every path before uploading any, so one bad path can't leave the
|
|
17
|
+
* others uploaded and unsent. */
|
|
18
|
+
export declare function checkFiles(paths: string[]): FileProblem[];
|
|
19
|
+
/** Upload in order; the returned ids go in the ping's `attachmentIds`. */
|
|
20
|
+
export declare function uploadPingFiles(config: CodiedevConfig, paths: string[]): Promise<string[]>;
|
|
21
|
+
/** Validate, then upload. Prints the problems and exits on a bad path. */
|
|
22
|
+
export declare function uploadOrExit(config: CodiedevConfig, paths: string[]): Promise<string[]>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_PING_FILE_BYTES = exports.MAX_PING_FILES = void 0;
|
|
7
|
+
exports.contentTypeFor = contentTypeFor;
|
|
8
|
+
exports.formatBytes = formatBytes;
|
|
9
|
+
exports.takeFileArgs = takeFileArgs;
|
|
10
|
+
exports.checkFiles = checkFiles;
|
|
11
|
+
exports.uploadPingFiles = uploadPingFiles;
|
|
12
|
+
exports.uploadOrExit = uploadOrExit;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const http_1 = __importDefault(require("http"));
|
|
15
|
+
const https_1 = __importDefault(require("https"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const version_1 = require("../version");
|
|
18
|
+
// Files sent with `codiedev ping --file` / `codiedev reply --file`. The server
|
|
19
|
+
// stores each one and hands back an id; the ping then names those ids. Limits
|
|
20
|
+
// mirror the server's so a too-large file fails here, before the upload.
|
|
21
|
+
exports.MAX_PING_FILES = 10;
|
|
22
|
+
exports.MAX_PING_FILE_BYTES = 20 * 1024 * 1024;
|
|
23
|
+
const CONTENT_TYPES = {
|
|
24
|
+
csv: "text/csv",
|
|
25
|
+
tsv: "text/tab-separated-values",
|
|
26
|
+
txt: "text/plain",
|
|
27
|
+
log: "text/plain",
|
|
28
|
+
md: "text/markdown",
|
|
29
|
+
json: "application/json",
|
|
30
|
+
yaml: "text/plain",
|
|
31
|
+
yml: "text/plain",
|
|
32
|
+
diff: "text/plain",
|
|
33
|
+
patch: "text/plain",
|
|
34
|
+
png: "image/png",
|
|
35
|
+
jpg: "image/jpeg",
|
|
36
|
+
jpeg: "image/jpeg",
|
|
37
|
+
gif: "image/gif",
|
|
38
|
+
webp: "image/webp",
|
|
39
|
+
pdf: "application/pdf",
|
|
40
|
+
zip: "application/zip",
|
|
41
|
+
};
|
|
42
|
+
function contentTypeFor(fileName) {
|
|
43
|
+
const ext = path_1.default.extname(fileName).slice(1).toLowerCase();
|
|
44
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
45
|
+
}
|
|
46
|
+
function formatBytes(bytes) {
|
|
47
|
+
if (bytes < 1024)
|
|
48
|
+
return `${bytes} B`;
|
|
49
|
+
if (bytes < 1024 * 1024)
|
|
50
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
51
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
52
|
+
}
|
|
53
|
+
/** Pull every `--file <path>` / `-f <path>` / `--file=<path>` out of argv,
|
|
54
|
+
* returning the paths and the remaining args untouched. */
|
|
55
|
+
function takeFileArgs(args) {
|
|
56
|
+
const files = [];
|
|
57
|
+
const rest = [];
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const a = args[i];
|
|
60
|
+
if ((a === "--file" || a === "-f") && i + 1 < args.length)
|
|
61
|
+
files.push(args[++i]);
|
|
62
|
+
else if (a.startsWith("--file="))
|
|
63
|
+
files.push(a.slice("--file=".length));
|
|
64
|
+
else
|
|
65
|
+
rest.push(a);
|
|
66
|
+
}
|
|
67
|
+
return { files, rest };
|
|
68
|
+
}
|
|
69
|
+
/** Check every path before uploading any, so one bad path can't leave the
|
|
70
|
+
* others uploaded and unsent. */
|
|
71
|
+
function checkFiles(paths) {
|
|
72
|
+
const problems = [];
|
|
73
|
+
if (paths.length > exports.MAX_PING_FILES) {
|
|
74
|
+
problems.push({ path: "--file", reason: `at most ${exports.MAX_PING_FILES} files per message` });
|
|
75
|
+
}
|
|
76
|
+
for (const p of paths) {
|
|
77
|
+
let stat;
|
|
78
|
+
try {
|
|
79
|
+
stat = fs_1.default.statSync(p);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
problems.push({ path: p, reason: "file not found" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!stat.isFile())
|
|
86
|
+
problems.push({ path: p, reason: "not a file" });
|
|
87
|
+
else if (stat.size === 0)
|
|
88
|
+
problems.push({ path: p, reason: "file is empty" });
|
|
89
|
+
else if (stat.size > exports.MAX_PING_FILE_BYTES) {
|
|
90
|
+
problems.push({ path: p, reason: `larger than 20 MB (${formatBytes(stat.size)})` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return problems;
|
|
94
|
+
}
|
|
95
|
+
function uploadOne(config, filePath) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const name = path_1.default.basename(filePath);
|
|
98
|
+
const data = fs_1.default.readFileSync(filePath);
|
|
99
|
+
const url = new URL("/api/cli/ping-attachment", config.backendUrl);
|
|
100
|
+
url.searchParams.set("name", name);
|
|
101
|
+
const lib = url.protocol === "https:" ? https_1.default : http_1.default;
|
|
102
|
+
const req = lib.request({
|
|
103
|
+
hostname: url.hostname,
|
|
104
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
105
|
+
path: url.pathname + url.search,
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${config.token}`,
|
|
109
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
110
|
+
"Content-Type": contentTypeFor(name),
|
|
111
|
+
"Content-Length": data.length,
|
|
112
|
+
},
|
|
113
|
+
}, (res) => {
|
|
114
|
+
let chunks = "";
|
|
115
|
+
res.on("data", (c) => (chunks += c));
|
|
116
|
+
res.on("end", () => {
|
|
117
|
+
let parsed = {};
|
|
118
|
+
try {
|
|
119
|
+
parsed = chunks ? JSON.parse(chunks) : {};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* fall through to the status check */
|
|
123
|
+
}
|
|
124
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300 && parsed.attachment?.id) {
|
|
125
|
+
resolve(parsed.attachment.id);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
reject(new Error(`${name}: ${parsed.error ?? `HTTP ${res.statusCode}`}`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
req.on("error", (err) => reject(new Error(`${name}: ${err.message}`)));
|
|
133
|
+
req.write(data);
|
|
134
|
+
req.end();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Upload in order; the returned ids go in the ping's `attachmentIds`. */
|
|
138
|
+
async function uploadPingFiles(config, paths) {
|
|
139
|
+
const ids = [];
|
|
140
|
+
for (const p of paths)
|
|
141
|
+
ids.push(await uploadOne(config, p));
|
|
142
|
+
return ids;
|
|
143
|
+
}
|
|
144
|
+
/** Validate, then upload. Prints the problems and exits on a bad path. */
|
|
145
|
+
async function uploadOrExit(config, paths) {
|
|
146
|
+
if (paths.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
const problems = checkFiles(paths);
|
|
149
|
+
if (problems.length) {
|
|
150
|
+
for (const p of problems)
|
|
151
|
+
console.error(`Can't attach ${p.path}: ${p.reason}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
return await uploadPingFiles(config, paths);
|
|
155
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runReply = runReply;
|
|
4
|
+
const shared_1 = require("./shared");
|
|
5
|
+
const pingFiles_1 = require("./pingFiles");
|
|
6
|
+
const USAGE = 'Usage: codiedev reply <ping-id> "<message>" [--file <path>]';
|
|
7
|
+
/**
|
|
8
|
+
* Answer a ping. Unlike a fresh `codiedev ping` back, this threads under the
|
|
9
|
+
* original and marks it read + replied, so it stops counting as waiting.
|
|
10
|
+
*/
|
|
11
|
+
async function runReply(args) {
|
|
12
|
+
const { files, rest } = (0, pingFiles_1.takeFileArgs)(args);
|
|
13
|
+
const positional = rest.filter((a) => !a.startsWith("--"));
|
|
14
|
+
const pingId = positional[0];
|
|
15
|
+
// Join the rest in case the shell split the message despite quoting.
|
|
16
|
+
const body = (0, shared_1.stripQuotes)(positional.slice(1).join(" "));
|
|
17
|
+
if (!pingId || (!body && files.length === 0)) {
|
|
18
|
+
console.error(USAGE);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const config = (0, shared_1.requireConfig)();
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
try {
|
|
24
|
+
const attachmentIds = await (0, pingFiles_1.uploadOrExit)(config, files);
|
|
25
|
+
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reply", {
|
|
26
|
+
config,
|
|
27
|
+
body: { pingId, body, ...(attachmentIds.length ? { attachmentIds } : {}) },
|
|
28
|
+
});
|
|
29
|
+
(0, shared_1.recordAgentEvent)(config, {
|
|
30
|
+
tool: "codiedev_reply",
|
|
31
|
+
ok: true,
|
|
32
|
+
latencyMs: Date.now() - start,
|
|
33
|
+
});
|
|
34
|
+
const to = res.recipient ? `${res.recipient.name} <${res.recipient.email}>` : "them";
|
|
35
|
+
const withFiles = files.length ? ` with ${files.length} file${files.length === 1 ? "" : "s"}` : "";
|
|
36
|
+
console.log(`✓ Replied to ${to}${withFiles}`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const e = err;
|
|
40
|
+
(0, shared_1.recordAgentEvent)(config, {
|
|
41
|
+
tool: "codiedev_reply",
|
|
42
|
+
ok: false,
|
|
43
|
+
latencyMs: Date.now() - start,
|
|
44
|
+
error: e.message,
|
|
45
|
+
});
|
|
46
|
+
if (e.status === 404 && /ping not found/.test(e.message)) {
|
|
47
|
+
console.error(`No ping ${pingId} addressed to you. Run 'codiedev inbox' for the ids.`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.error(`Reply failed: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -54,6 +54,7 @@ const selfUpdate_1 = require("./selfUpdate");
|
|
|
54
54
|
const version_1 = require("./version");
|
|
55
55
|
// --- starter packs (SIG-113 phase 2): autosync, nudge ---
|
|
56
56
|
const sync_1 = require("./packs/sync");
|
|
57
|
+
const unreadNotice_1 = require("./unreadNotice");
|
|
57
58
|
// --- end starter packs ---
|
|
58
59
|
const captureLog_1 = require("./captureLog");
|
|
59
60
|
const subagents_1 = require("./subagents");
|
|
@@ -267,25 +268,28 @@ async function postSessionSkip(config, sessionId, reason) {
|
|
|
267
268
|
}
|
|
268
269
|
// SessionStart: the capture-coverage denominator. Record that a session opened
|
|
269
270
|
// so the backend can later compute captured ÷ started. Deliberately tiny — no
|
|
270
|
-
// transcript read, no git — so it never slows session startup. Best-effort
|
|
271
|
-
//
|
|
271
|
+
// transcript read, no git — so it never slows session startup. Best-effort.
|
|
272
|
+
// The response may carry the person's unread-ping summary, which the caller
|
|
273
|
+
// turns into a one-line notice; it rides on this request so the reminder
|
|
274
|
+
// costs no extra round trip.
|
|
272
275
|
async function runSessionStart(hookInput, source = "claude-code") {
|
|
273
276
|
const config = (0, utils_1.readConfig)();
|
|
274
277
|
if (!config)
|
|
275
|
-
return;
|
|
278
|
+
return undefined;
|
|
276
279
|
if (hookInput.session_id) {
|
|
277
280
|
try {
|
|
278
|
-
await
|
|
281
|
+
const res = await postJsonForResponse(`${config.backendUrl}/api/cli/sessionStart`, {
|
|
279
282
|
sessionId: hookInput.session_id,
|
|
280
283
|
startedAt: Date.now(),
|
|
281
284
|
source,
|
|
282
285
|
}, config.token, PING_TIMEOUT_MS);
|
|
286
|
+
return res?.unreadPings;
|
|
283
287
|
}
|
|
284
288
|
catch {
|
|
285
289
|
/* best-effort — never block session startup */
|
|
286
290
|
}
|
|
287
291
|
}
|
|
288
|
-
return;
|
|
292
|
+
return undefined;
|
|
289
293
|
}
|
|
290
294
|
function postJson(url, body, bearerToken,
|
|
291
295
|
// Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
|
|
@@ -337,7 +341,10 @@ timeoutMs) {
|
|
|
337
341
|
// postJson variant that returns the parsed response body — the upload-URL
|
|
338
342
|
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
339
343
|
// only in detached uploader children, never on the shutdown clock.
|
|
340
|
-
function postJsonForResponse(url, body, bearerToken
|
|
344
|
+
function postJsonForResponse(url, body, bearerToken,
|
|
345
|
+
// Socket timeout for tiny pings on the session-start path: a black-holed
|
|
346
|
+
// connection must not hang the hook. Unset = no timeout, as before.
|
|
347
|
+
timeoutMs) {
|
|
341
348
|
return new Promise((resolve, reject) => {
|
|
342
349
|
const parsed = new URL(url);
|
|
343
350
|
const data = JSON.stringify(body);
|
|
@@ -371,6 +378,11 @@ function postJsonForResponse(url, body, bearerToken) {
|
|
|
371
378
|
});
|
|
372
379
|
});
|
|
373
380
|
req.on("error", reject);
|
|
381
|
+
if (timeoutMs) {
|
|
382
|
+
req.setTimeout(timeoutMs, () => {
|
|
383
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms`));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
374
386
|
req.write(data);
|
|
375
387
|
req.end();
|
|
376
388
|
});
|
|
@@ -658,7 +670,7 @@ async function main() {
|
|
|
658
670
|
if (packNag && process.argv[2] === "session-start")
|
|
659
671
|
process.stdout.write(`${packNag}\n`);
|
|
660
672
|
// --- end starter packs ---
|
|
661
|
-
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
673
|
+
const unreadPings = await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
|
|
662
674
|
? "opencode"
|
|
663
675
|
: process.argv[2] === "session-start-cursor"
|
|
664
676
|
? "cursor"
|
|
@@ -667,13 +679,21 @@ async function main() {
|
|
|
667
679
|
: process.argv[2] === "session-start-pi"
|
|
668
680
|
? "pi"
|
|
669
681
|
: "claude-code");
|
|
682
|
+
// "A teammate is waiting on you." Claude Code reads SessionStart stdout
|
|
683
|
+
// into the session; Cursor and Copilot get it in their context below.
|
|
684
|
+
// OpenCode and pi ignore this hook's output, and Codex has no SessionStart.
|
|
685
|
+
const unreadNotice = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi"
|
|
686
|
+
? null
|
|
687
|
+
: (0, unreadNotice_1.takeUnreadNotice)(unreadPings);
|
|
688
|
+
if (unreadNotice && process.argv[2] === "session-start")
|
|
689
|
+
process.stdout.write(`${unreadNotice}\n`);
|
|
670
690
|
if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
|
|
671
691
|
const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
|
|
672
692
|
const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
|
|
673
693
|
const prompt = hookInput.initialPrompt
|
|
674
694
|
?? hookInput.initial_prompt;
|
|
675
695
|
const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
|
|
676
|
-
process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag].filter(Boolean).join("\n\n"))));
|
|
696
|
+
process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag, unreadNotice].filter(Boolean).join("\n\n"))));
|
|
677
697
|
}
|
|
678
698
|
return;
|
|
679
699
|
}
|
package/dist/inboxFormat.d.ts
CHANGED
|
@@ -11,7 +11,17 @@ export interface InboxPing {
|
|
|
11
11
|
title?: string;
|
|
12
12
|
} | null;
|
|
13
13
|
preview: string;
|
|
14
|
+
attachments?: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
size: number;
|
|
17
|
+
url?: string | null;
|
|
18
|
+
}>;
|
|
14
19
|
}
|
|
20
|
+
/** "shard-timings.csv (66 B), trace.log (1.2 MB)" */
|
|
21
|
+
export declare function fileList(files: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
size: number;
|
|
24
|
+
}>): string;
|
|
15
25
|
export interface InboxMention {
|
|
16
26
|
mentionId: string;
|
|
17
27
|
createdAt: number;
|
package/dist/inboxFormat.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileList = fileList;
|
|
3
4
|
exports.formatInbox = formatInbox;
|
|
4
5
|
const shared_1 = require("./commands/shared");
|
|
6
|
+
function fileSize(bytes) {
|
|
7
|
+
if (bytes < 1024)
|
|
8
|
+
return `${bytes} B`;
|
|
9
|
+
if (bytes < 1024 * 1024)
|
|
10
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
11
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
12
|
+
}
|
|
13
|
+
/** "shard-timings.csv (66 B), trace.log (1.2 MB)" */
|
|
14
|
+
function fileList(files) {
|
|
15
|
+
return files.map((f) => `${f.name} (${fileSize(f.size)})`).join(", ");
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Render the inbox for BOTH surfaces — `codiedev inbox` and the MCP
|
|
7
19
|
* `codiedev_inbox` tool an agent calls for "any messages?". Shared so the
|
|
@@ -47,6 +59,9 @@ function formatInbox(opts) {
|
|
|
47
59
|
const where = p.subjectKey ? ` · on ${p.subjectKey}` : "";
|
|
48
60
|
lines.push(`${dot} ${from}${where} · ${(0, shared_1.timeAgo)(p.createdAt)}`);
|
|
49
61
|
lines.push(` ${p.preview || "(no body)"}`);
|
|
62
|
+
if (p.attachments?.length) {
|
|
63
|
+
lines.push(` ${p.attachments.length === 1 ? "file" : "files"}: ${fileList(p.attachments)}`);
|
|
64
|
+
}
|
|
50
65
|
lines.push(` id: ${p.pingId}`);
|
|
51
66
|
lines.push("");
|
|
52
67
|
}
|
|
@@ -63,7 +78,9 @@ function formatInbox(opts) {
|
|
|
63
78
|
lines.push(` ${m.url}`);
|
|
64
79
|
lines.push("");
|
|
65
80
|
}
|
|
66
|
-
if (keptPings.length > 0)
|
|
81
|
+
if (keptPings.length > 0) {
|
|
67
82
|
lines.push("Mark read: codiedev read <ping-id>");
|
|
83
|
+
lines.push('Reply: codiedev reply <ping-id> "<message>"');
|
|
84
|
+
}
|
|
68
85
|
return lines;
|
|
69
86
|
}
|
package/dist/launcher.d.ts
CHANGED
|
@@ -8,9 +8,14 @@ export declare function launcherPath(): string;
|
|
|
8
8
|
/** True iff `cmd` invokes the stable launcher (from any state dir). */
|
|
9
9
|
export declare function usesLauncher(cmd: string | undefined): boolean;
|
|
10
10
|
export declare function readInstallRecord(): InstallRecord | null;
|
|
11
|
+
/** True if `p` is inside the OS temp folder (real paths, so /var vs /private/var on macOS match). */
|
|
12
|
+
export declare function isInTempDir(p: string, tmp?: string): boolean;
|
|
11
13
|
/**
|
|
12
14
|
* The hook script worth recording: absolute and present, and a global install over an npx cache
|
|
13
15
|
* copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
|
|
16
|
+
* Never a build in the temp folder: test runs stage one there, delete it, and if that landed in
|
|
17
|
+
* the real install.json every hook went quietly dead. CODIEDEV_ALLOW_TEMP_HOOK=1 (the CLI's own
|
|
18
|
+
* test setup) lets sandboxed tests use one.
|
|
14
19
|
*/
|
|
15
20
|
export declare function stableHookScript(hookScript: string, globalCandidates?: string[]): string | null;
|
|
16
21
|
/** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
|
package/dist/launcher.js
CHANGED
|
@@ -48,16 +48,18 @@ exports.LAUNCHER_FILE = void 0;
|
|
|
48
48
|
exports.launcherPath = launcherPath;
|
|
49
49
|
exports.usesLauncher = usesLauncher;
|
|
50
50
|
exports.readInstallRecord = readInstallRecord;
|
|
51
|
+
exports.isInTempDir = isInTempDir;
|
|
51
52
|
exports.stableHookScript = stableHookScript;
|
|
52
53
|
exports.recordCurrentInstall = recordCurrentInstall;
|
|
53
54
|
exports.ensureLauncher = ensureLauncher;
|
|
54
55
|
exports.resetLauncherCacheForTests = resetLauncherCacheForTests;
|
|
55
56
|
const fs = __importStar(require("fs"));
|
|
57
|
+
const os = __importStar(require("os"));
|
|
56
58
|
const path = __importStar(require("path"));
|
|
57
59
|
const configDir_1 = require("./configDir");
|
|
58
60
|
exports.LAUNCHER_FILE = "codiedev-hook-launcher.cjs";
|
|
59
61
|
// Bump when LAUNCHER_SOURCE changes so existing launchers get rewritten.
|
|
60
|
-
const LAUNCHER_REVISION =
|
|
62
|
+
const LAUNCHER_REVISION = 2;
|
|
61
63
|
function launcherPath() {
|
|
62
64
|
return path.join((0, configDir_1.codiedevStateDir)(), "bin", exports.LAUNCHER_FILE);
|
|
63
65
|
}
|
|
@@ -85,6 +87,12 @@ try {
|
|
|
85
87
|
if (rec && typeof rec.hookScript === "string" && fs.existsSync(rec.hookScript)) target = rec.hookScript;
|
|
86
88
|
} catch {}
|
|
87
89
|
if (!target && fs.existsSync(FALLBACK)) target = FALLBACK;
|
|
90
|
+
if (!target) {
|
|
91
|
+
// Both stranded (e.g. a deleted temp build): use a global install next to this node.
|
|
92
|
+
const prefix = path.dirname(path.dirname(process.execPath));
|
|
93
|
+
target = [path.join(prefix, "lib", "node_modules", "codiedev", "dist", "hook.js"),
|
|
94
|
+
path.join(prefix, "node_modules", "codiedev", "dist", "hook.js")].find((p) => fs.existsSync(p)) || null;
|
|
95
|
+
}
|
|
88
96
|
if (!target) process.exit(0);
|
|
89
97
|
process.env.CODIEDEV_VIA_LAUNCHER = "1";
|
|
90
98
|
process.argv[1] = target;
|
|
@@ -115,15 +123,34 @@ function packageVersionFor(hookScript) {
|
|
|
115
123
|
return undefined;
|
|
116
124
|
}
|
|
117
125
|
}
|
|
126
|
+
/** True if `p` is inside the OS temp folder (real paths, so /var vs /private/var on macOS match). */
|
|
127
|
+
function isInTempDir(p, tmp = os.tmpdir()) {
|
|
128
|
+
const real = (x) => {
|
|
129
|
+
try {
|
|
130
|
+
return fs.realpathSync(x);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return path.resolve(x);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const rel = path.relative(real(tmp), real(p));
|
|
137
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
138
|
+
}
|
|
118
139
|
/**
|
|
119
140
|
* The hook script worth recording: absolute and present, and a global install over an npx cache
|
|
120
141
|
* copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
|
|
142
|
+
* Never a build in the temp folder: test runs stage one there, delete it, and if that landed in
|
|
143
|
+
* the real install.json every hook went quietly dead. CODIEDEV_ALLOW_TEMP_HOOK=1 (the CLI's own
|
|
144
|
+
* test setup) lets sandboxed tests use one.
|
|
121
145
|
*/
|
|
122
146
|
function stableHookScript(hookScript, globalCandidates = globalHookScripts()) {
|
|
123
147
|
if (!hookScript || !path.isAbsolute(hookScript) || hookScript.includes(`${path.sep}~${path.sep}`))
|
|
124
148
|
return null;
|
|
125
149
|
if (!fs.existsSync(hookScript))
|
|
126
150
|
return null;
|
|
151
|
+
if (process.env.CODIEDEV_ALLOW_TEMP_HOOK !== "1" && isInTempDir(hookScript)) {
|
|
152
|
+
return globalCandidates.find((p) => path.isAbsolute(p) && fs.existsSync(p) && !isInTempDir(p)) ?? null;
|
|
153
|
+
}
|
|
127
154
|
if (hookScript.includes(`${path.sep}_npx${path.sep}`)) {
|
|
128
155
|
const global = globalCandidates.find((p) => path.isAbsolute(p) && fs.existsSync(p));
|
|
129
156
|
if (global)
|
package/dist/mcp.js
CHANGED
|
@@ -291,9 +291,7 @@ const TOOLS = [
|
|
|
291
291
|
"when the user asks 'has anyone solved X?', 'find me the spec on Y', " +
|
|
292
292
|
"or 'look for prior work on Z'. Always search before pushing a new " +
|
|
293
293
|
"artifact so the user doesn't duplicate work. Returns ranked hits " +
|
|
294
|
-
"with title, type, filename key, and a snippet.
|
|
295
|
-
"questions where the user wants a synthesized answer (not just a list " +
|
|
296
|
-
"of hits), prefer codiedev_ask instead.",
|
|
294
|
+
"with title, type, filename key, and a snippet.",
|
|
297
295
|
inputSchema: {
|
|
298
296
|
type: "object",
|
|
299
297
|
properties: {
|
|
@@ -303,32 +301,6 @@ const TOOLS = [
|
|
|
303
301
|
required: ["query"],
|
|
304
302
|
},
|
|
305
303
|
},
|
|
306
|
-
{
|
|
307
|
-
name: "codiedev_ask",
|
|
308
|
-
description: "Ask a natural-language question of the team's collective memory and " +
|
|
309
|
-
"get a synthesized answer with citations. Searches across both " +
|
|
310
|
-
"auto-extracted artifacts (specs / decisions / bugfixes) AND raw " +
|
|
311
|
-
"session transcripts, then has an LLM compose an answer that cites " +
|
|
312
|
-
"the actual sources. Use when the user asks 'why did we…?', 'what " +
|
|
313
|
-
"does the team know about X?', 'has anyone done Y here before?', " +
|
|
314
|
-
"'who decided Z and why?', or any open-ended question about prior " +
|
|
315
|
-
"work where they want context, not just a hit list. For exact-match " +
|
|
316
|
-
"lookups (find me spec-x), prefer codiedev_pull. For browsing or " +
|
|
317
|
-
"filtering by kind/tag, prefer codiedev_get_library. For surfacing " +
|
|
318
|
-
"ranked hits without LLM synthesis, prefer codiedev_search.",
|
|
319
|
-
inputSchema: {
|
|
320
|
-
type: "object",
|
|
321
|
-
properties: {
|
|
322
|
-
question: {
|
|
323
|
-
type: "string",
|
|
324
|
-
description: "Natural-language question. Open-ended is fine — e.g. 'why " +
|
|
325
|
-
"did we drop pg-bouncer?' or 'how does the team think about " +
|
|
326
|
-
"feature flags?'.",
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
required: ["question"],
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
304
|
{
|
|
333
305
|
name: "codiedev_get_library",
|
|
334
306
|
description: "List artifacts in the team library — both docs (spec / decision / " +
|
|
@@ -467,8 +439,6 @@ async function dispatchTool(name, args, config) {
|
|
|
467
439
|
return await handleShareWith(args, config);
|
|
468
440
|
case "codiedev_send_to":
|
|
469
441
|
return await handleSendTo(args, config);
|
|
470
|
-
case "codiedev_ask":
|
|
471
|
-
return await handleAsk(args, config);
|
|
472
442
|
case "codiedev_search":
|
|
473
443
|
return await handleSearch(args, config);
|
|
474
444
|
case "codiedev_get_library":
|
|
@@ -740,23 +710,6 @@ async function handleSearch(args, config) {
|
|
|
740
710
|
}
|
|
741
711
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
742
712
|
}
|
|
743
|
-
async function handleAsk(args, config) {
|
|
744
|
-
const question = asString(args.question);
|
|
745
|
-
if (!question)
|
|
746
|
-
throw new Error("question required");
|
|
747
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", { config, body: { question } });
|
|
748
|
-
const lines = [res.answer];
|
|
749
|
-
if (res.citations && res.citations.length > 0) {
|
|
750
|
-
lines.push("", "Sources:");
|
|
751
|
-
for (const c of res.citations) {
|
|
752
|
-
const label = c.kind === "artifact"
|
|
753
|
-
? `[${c.type}] ${c.title}`
|
|
754
|
-
: `[session] ${c.title}`;
|
|
755
|
-
lines.push(` • ${label}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
759
|
-
}
|
|
760
713
|
async function handleGetLibrary(args, config) {
|
|
761
714
|
const scope = asStringOrUndefined(args.scope);
|
|
762
715
|
const type = asStringOrUndefined(args.type);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Session-start context uses each client's supported output contract. */
|
|
2
|
-
export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked,
|
|
2
|
+
export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
|
|
3
3
|
export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
|
|
4
4
|
additional_context: string;
|
|
5
5
|
additionalContext?: undefined;
|
package/dist/teamSkillContext.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.teamSkillSessionOutput = teamSkillSessionOutput;
|
|
|
5
5
|
/** Session-start context uses each client's supported output contract. */
|
|
6
6
|
exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
|
|
7
7
|
Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
|
|
8
|
-
Use codiedev push <file.md> to share an artifact when asked,
|
|
8
|
+
Use codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
|
|
9
9
|
function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
|
|
10
10
|
return source === "cursor" ? { additional_context: context } : { additionalContext: context };
|
|
11
11
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface UnreadPings {
|
|
2
|
+
count: number;
|
|
3
|
+
newestPingId: string | null;
|
|
4
|
+
from: string[];
|
|
5
|
+
senderCount?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Repeat an unchanged notice at most this often. A new ping always shows. */
|
|
8
|
+
export declare const UNREAD_NOTICE_INTERVAL_MS: number;
|
|
9
|
+
export declare function parseUnreadPings(value: unknown): UnreadPings | null;
|
|
10
|
+
/** The line added to the session's context. Written for the agent: it says
|
|
11
|
+
* what is waiting and what to do about it. */
|
|
12
|
+
export declare function unreadNoticeLine(u: UnreadPings): string;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the notice to print, or null. Shown when the newest unread ping is
|
|
15
|
+
* one we haven't announced, or the same pile is still there a day later.
|
|
16
|
+
* Records that it spoke; any failure is silent — never slow or break a
|
|
17
|
+
* session start over a reminder.
|
|
18
|
+
*/
|
|
19
|
+
export declare function takeUnreadNotice(value: unknown, options?: {
|
|
20
|
+
dir?: string;
|
|
21
|
+
now?: number;
|
|
22
|
+
}): string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UNREAD_NOTICE_INTERVAL_MS = void 0;
|
|
7
|
+
exports.parseUnreadPings = parseUnreadPings;
|
|
8
|
+
exports.unreadNoticeLine = unreadNoticeLine;
|
|
9
|
+
exports.takeUnreadNotice = takeUnreadNotice;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const configDir_1 = require("./configDir");
|
|
13
|
+
/** Repeat an unchanged notice at most this often. A new ping always shows. */
|
|
14
|
+
exports.UNREAD_NOTICE_INTERVAL_MS = 20 * 60 * 60 * 1000;
|
|
15
|
+
const STATE_FILE = "unread-notice.json";
|
|
16
|
+
function parseUnreadPings(value) {
|
|
17
|
+
if (!value || typeof value !== "object")
|
|
18
|
+
return null;
|
|
19
|
+
const v = value;
|
|
20
|
+
if (typeof v.count !== "number" || !Number.isFinite(v.count) || v.count <= 0)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
count: Math.floor(v.count),
|
|
24
|
+
newestPingId: typeof v.newestPingId === "string" ? v.newestPingId : null,
|
|
25
|
+
from: Array.isArray(v.from) ? v.from.filter((n) => typeof n === "string" && n.trim() !== "") : [],
|
|
26
|
+
senderCount: typeof v.senderCount === "number" ? v.senderCount : undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function nameList(from, senderCount) {
|
|
30
|
+
if (from.length === 0)
|
|
31
|
+
return "";
|
|
32
|
+
const others = Math.max(0, senderCount - from.length);
|
|
33
|
+
const names = others > 0 ? [...from, `${others} other${others === 1 ? "" : "s"}`] : from;
|
|
34
|
+
if (names.length === 1)
|
|
35
|
+
return ` from ${names[0]}`;
|
|
36
|
+
return ` from ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
37
|
+
}
|
|
38
|
+
/** The line added to the session's context. Written for the agent: it says
|
|
39
|
+
* what is waiting and what to do about it. */
|
|
40
|
+
function unreadNoticeLine(u) {
|
|
41
|
+
const who = nameList(u.from, u.senderCount ?? u.from.length);
|
|
42
|
+
return (`Codiedev: ${u.count} unread message${u.count === 1 ? "" : "s"}${who}. ` +
|
|
43
|
+
"Tell the user at the start of your first reply, and offer to show them with `codiedev inbox --unread`. " +
|
|
44
|
+
'Answer one with `codiedev reply <ping-id> "<message>"`.');
|
|
45
|
+
}
|
|
46
|
+
function readState(dir) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, STATE_FILE), "utf8"));
|
|
49
|
+
if (typeof raw?.at !== "number")
|
|
50
|
+
return null;
|
|
51
|
+
return { newestPingId: typeof raw.newestPingId === "string" ? raw.newestPingId : null, at: raw.at };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns the notice to print, or null. Shown when the newest unread ping is
|
|
59
|
+
* one we haven't announced, or the same pile is still there a day later.
|
|
60
|
+
* Records that it spoke; any failure is silent — never slow or break a
|
|
61
|
+
* session start over a reminder.
|
|
62
|
+
*/
|
|
63
|
+
function takeUnreadNotice(value, options = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const unread = parseUnreadPings(value);
|
|
66
|
+
if (!unread)
|
|
67
|
+
return null;
|
|
68
|
+
const dir = options.dir ?? (0, configDir_1.codiedevStateDir)();
|
|
69
|
+
const now = options.now ?? Date.now();
|
|
70
|
+
const last = readState(dir);
|
|
71
|
+
const sameNewest = !!last && last.newestPingId === unread.newestPingId;
|
|
72
|
+
if (sameNewest && last.at <= now && now - last.at < exports.UNREAD_NOTICE_INTERVAL_MS)
|
|
73
|
+
return null;
|
|
74
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
75
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, STATE_FILE), JSON.stringify({ newestPingId: unread.newestPingId, at: now }));
|
|
76
|
+
return unreadNoticeLine(unread);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -1673,14 +1673,14 @@ thought, use the \`codiedev\` CLI via Bash:**
|
|
|
1673
1673
|
| "push this spec" / "save this as a skill" | \`codiedev push <file.md>\` |
|
|
1674
1674
|
| "pull X" / "grab the latest spec-X" / "what did a teammate push?" | \`codiedev pull <key>\` |
|
|
1675
1675
|
| "delete X" / "remove that test push" / "rm spec-old" | \`codiedev delete <key>\` (always confirm with the user first) |
|
|
1676
|
-
| "ping a teammate about this" / "ask someone for their take" | \`codiedev ping <name> "<msg>" [--with <key>]\` |
|
|
1676
|
+
| "ping a teammate about this" / "ask someone for their take" / "send them this file" | \`codiedev ping <name> "<msg>" [--with <key>] [--file <path>]\` |
|
|
1677
1677
|
| "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
|
|
1678
1678
|
| "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
|
|
1679
|
+
| "reply to that" / "tell them X" (answering a message from \`codiedev inbox\`) | \`codiedev reply <ping-id> "<msg>" [--file <path>]\` (not a new ping: this threads it and marks theirs answered) |
|
|
1679
1680
|
| "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
|
|
1680
1681
|
| "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
|
|
1681
1682
|
| "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
|
|
1682
1683
|
| "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
|
|
1683
|
-
| "why did we…?" / "what does the team know about X?" | \`codiedev ask "<question>"\` (synthesized answer + citations) |
|
|
1684
1684
|
| "show my library" / "what artifacts exist?" | \`codiedev library [--scope mine\\|shared\\|all] [--kind doc\\|skill]\` |
|
|
1685
1685
|
| "run my agent's test cases" / "check the dataset on model Y" / "run v2 of the dataset" | \`codiedev traces cases <agent> [--dataset <name> [--version <n>]] [--model <id>]\` (writes cases + RUN_CASES.md + results.json; follow it, then \`codiedev traces report <results.json>\`) |
|
|
1686
1686
|
| "how are my agents doing?" / "what's failing this week?" / "show the booking agent's failed runs" / "why did run X fail?" | \`codiedev traces overview\`; \`codiedev traces runs <agent> [--failed] [--flag <issue>] [--days n]\`; \`codiedev traces run <run> --agent <name>\` (add \`--json\` to read the data yourself) |
|
|
@@ -1695,9 +1695,10 @@ thought, use the \`codiedev\` CLI via Bash:**
|
|
|
1695
1695
|
is classified into one of two kinds at upload time:
|
|
1696
1696
|
|
|
1697
1697
|
- **skill** — the file has SKILL.md frontmatter (\`name:\` + \`description:\`
|
|
1698
|
-
inside a \`---\` block).
|
|
1698
|
+
inside a \`---\` block). Shows on the Skills page in the portal and is
|
|
1699
1699
|
invokable as a slash command (\`/<name>\`).
|
|
1700
|
-
- **doc** — everything else.
|
|
1700
|
+
- **doc** — everything else. Lives in the library: list it with
|
|
1701
|
+
\`codiedev library\`, fetch it with \`codiedev pull <key>\`.
|
|
1701
1702
|
|
|
1702
1703
|
Detection is content-based, not filename-based. The filename prefix
|
|
1703
1704
|
(\`spec-\` / \`decision-\` / \`proposal-\` / \`bugfix-\` / \`review-\` /
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.84",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
package/dist/commands/ask.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runAsk(args: string[]): Promise<void>;
|
package/dist/commands/ask.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runAsk = runAsk;
|
|
4
|
-
const shared_1 = require("./shared");
|
|
5
|
-
function parseArgs(args) {
|
|
6
|
-
// Everything after the command, joined back into a question string. Allows
|
|
7
|
-
// both quoted and unquoted forms: `codiedev ask "why pg-bouncer?"` or
|
|
8
|
-
// `codiedev ask why did we drop pg-bouncer`.
|
|
9
|
-
const question = args
|
|
10
|
-
.filter((a) => !a.startsWith("--"))
|
|
11
|
-
.join(" ")
|
|
12
|
-
.trim();
|
|
13
|
-
if (!question) {
|
|
14
|
-
console.error('Usage: codiedev ask "<question>"');
|
|
15
|
-
console.error('Example: codiedev ask "why did we drop pg-bouncer?"');
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
return { question };
|
|
19
|
-
}
|
|
20
|
-
async function runAsk(args) {
|
|
21
|
-
const { question } = parseArgs(args);
|
|
22
|
-
const config = (0, shared_1.requireConfig)();
|
|
23
|
-
const start = Date.now();
|
|
24
|
-
try {
|
|
25
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", {
|
|
26
|
-
config,
|
|
27
|
-
body: { question },
|
|
28
|
-
});
|
|
29
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
30
|
-
tool: "codiedev_ask",
|
|
31
|
-
ok: true,
|
|
32
|
-
latencyMs: Date.now() - start,
|
|
33
|
-
});
|
|
34
|
-
console.log("");
|
|
35
|
-
console.log(res.answer);
|
|
36
|
-
if (res.citations.length > 0) {
|
|
37
|
-
console.log("");
|
|
38
|
-
console.log("Sources:");
|
|
39
|
-
for (const c of res.citations) {
|
|
40
|
-
const label = c.kind === "artifact"
|
|
41
|
-
? `[${c.type}] ${c.title}`
|
|
42
|
-
: `[session] ${c.title}`;
|
|
43
|
-
console.log(` • ${label}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
console.log("");
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
50
|
-
tool: "codiedev_ask",
|
|
51
|
-
ok: false,
|
|
52
|
-
latencyMs: Date.now() - start,
|
|
53
|
-
error: err.message,
|
|
54
|
-
});
|
|
55
|
-
console.error(`Ask failed: ${err.message}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
}
|