codiedev 0.9.31 → 0.9.33
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/assets/git-refs.mjs +55 -11
- package/assets/opencode-plugin.mjs +22 -1
- package/dist/cli.js +5 -1
- package/dist/commands/inbox.d.ts +2 -1
- package/dist/commands/inbox.js +10 -21
- package/dist/inboxFormat.d.ts +38 -0
- package/dist/inboxFormat.js +69 -0
- package/dist/mcp.js +11 -27
- package/package.json +1 -1
package/assets/git-refs.mjs
CHANGED
|
@@ -7,8 +7,6 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { dirname, isAbsolute } from "node:path";
|
|
9
9
|
|
|
10
|
-
const skillUseBySession = new Map();
|
|
11
|
-
|
|
12
10
|
/**
|
|
13
11
|
* The directory the agent last WROTE to. Reading refs from the plugin's cwd is
|
|
14
12
|
* only right when `codie` was launched where the work lands. A dev who launches
|
|
@@ -27,15 +25,61 @@ const skillUseBySession = new Map();
|
|
|
27
25
|
* catch it.
|
|
28
26
|
*/
|
|
29
27
|
export const workDirBySession = new Map();
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Tool ids as OpenCode actually registers them, captured from a live run — not
|
|
30
|
+
* read off the binary, which is what got this wrong twice. The patch tool is
|
|
31
|
+
* `apply_patch`; `patch` is kept only as a defensive alias.
|
|
32
|
+
*/
|
|
33
|
+
const WRITE_TOOLS = new Set(["write", "edit", "apply_patch", "patch"]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which directory a tool call says the agent is working in, or null.
|
|
37
|
+
*
|
|
38
|
+
* Covering only `write`/`edit` was not enough. Two live `codie run` sessions
|
|
39
|
+
* were driven to create a file and the agent picked `bash` once and `patch`
|
|
40
|
+
* once — neither carries a `filePath` in its args, so both sessions reported
|
|
41
|
+
* the launch branch:
|
|
42
|
+
*
|
|
43
|
+
* write / edit args.filePath — known before the write
|
|
44
|
+
* apply_patch result.metadata.files[].filePath — only known AFTER it runs
|
|
45
|
+
*
|
|
46
|
+
* `bash` is deliberately NOT handled. Honouring its `workdir` re-anchors on
|
|
47
|
+
* READS too — `bash {command:"git log", workdir:"/repo"}` would move the
|
|
48
|
+
* anchor off a tracked worktree and back to the primary checkout, which
|
|
49
|
+
* passes the trust check because both share an origin. That breaks the rule
|
|
50
|
+
* this module is built on: writes are the signal, reads are not. Mining the
|
|
51
|
+
* command text was tried instead and did not work live. See NEXT_RELEASE.md.
|
|
52
|
+
*
|
|
53
|
+
* `patch` takes `patchText`, so its args never name a file. Shape confirmed
|
|
54
|
+
* against the opencode binary:
|
|
55
|
+
* metadata:{files:[{type:"update",filePath:…,relativePath:…,diff:…}]}
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
function workDirFromTool(tool, args, result) {
|
|
59
|
+
if (!WRITE_TOOLS.has(tool)) return null;
|
|
60
|
+
const candidates = [
|
|
61
|
+
args?.filePath,
|
|
62
|
+
args?.file_path,
|
|
63
|
+
...(Array.isArray(result?.metadata?.files) ? result.metadata.files : []).flatMap((file) => [
|
|
64
|
+
file?.movePath, // a move reports the destination, which is where the work is
|
|
65
|
+
file?.filePath,
|
|
66
|
+
]),
|
|
67
|
+
];
|
|
68
|
+
// relativePath is deliberately excluded — it resolves against a cwd we may
|
|
69
|
+
// not share, so it is not a usable anchor.
|
|
70
|
+
const hit = candidates.find((path) => typeof path === "string" && isAbsolute(path));
|
|
71
|
+
return hit ? dirname(hit) : null;
|
|
72
|
+
}
|
|
31
73
|
|
|
32
|
-
export function trackWorkPath(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
export function trackWorkPath({
|
|
75
|
+
tool,
|
|
76
|
+
args,
|
|
77
|
+
sessionID,
|
|
78
|
+
result = null,
|
|
79
|
+
state = workDirBySession,
|
|
80
|
+
}) {
|
|
81
|
+
const dir = workDirFromTool(tool, args, result);
|
|
82
|
+
if (dir) state.set(sessionID, dir);
|
|
39
83
|
}
|
|
40
84
|
|
|
41
85
|
/**
|
|
@@ -93,7 +137,7 @@ function normalizeRemote(url) {
|
|
|
93
137
|
const GIT_TTL_MS = 5_000;
|
|
94
138
|
const gitCache = {};
|
|
95
139
|
|
|
96
|
-
function clean(value, max) {
|
|
140
|
+
export function clean(value, max) {
|
|
97
141
|
const text = typeof value === "string" ? value.trim() : "";
|
|
98
142
|
return text ? text.slice(0, max) : "";
|
|
99
143
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// names to inference requests handled by the company gateway.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
clean,
|
|
7
8
|
buildAttributionHeaders,
|
|
8
9
|
resolveGitRefs,
|
|
9
10
|
trackWorkPath,
|
|
@@ -34,10 +35,30 @@ export const CodiedevMetadata = async () => ({
|
|
|
34
35
|
// Follow where the agent writes so refs come from the tree that becomes
|
|
35
36
|
// the PR. Only the path's DIRECTORY is kept, and only to run git in it —
|
|
36
37
|
// no path ever leaves this process.
|
|
37
|
-
|
|
38
|
+
//
|
|
39
|
+
// `write`/`edit` name the file up front; `bash` may carry a workdir. Both
|
|
40
|
+
// are worth catching here so the very next request already reports the
|
|
41
|
+
// right branch.
|
|
42
|
+
trackWorkPath({
|
|
43
|
+
tool: input.tool,
|
|
44
|
+
args: output.args,
|
|
45
|
+
sessionID: input.sessionID,
|
|
46
|
+
});
|
|
38
47
|
if (input.tool !== "skill") return;
|
|
39
48
|
addSkill(input.sessionID, output.args?.name || output.args?.skill || output.args?.key);
|
|
40
49
|
},
|
|
50
|
+
"tool.execute.after": async (input, output) => {
|
|
51
|
+
// `patch` takes patchText, so the files it touched are only known once it
|
|
52
|
+
// has run — they arrive in the result's metadata.files[]. Without this
|
|
53
|
+
// hook a patch-driven session never moves off the launch branch, which is
|
|
54
|
+
// exactly what two live runs showed.
|
|
55
|
+
trackWorkPath({
|
|
56
|
+
tool: input.tool,
|
|
57
|
+
args: input.args,
|
|
58
|
+
sessionID: input.sessionID,
|
|
59
|
+
result: output,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
41
62
|
"chat.headers": async (input, output) => {
|
|
42
63
|
Object.assign(
|
|
43
64
|
output.headers,
|
package/dist/cli.js
CHANGED
|
@@ -228,9 +228,13 @@ codiedev inbox — show messages from teammates
|
|
|
228
228
|
Usage:
|
|
229
229
|
codiedev inbox [--unread] [--limit N]
|
|
230
230
|
|
|
231
|
-
Lists pings with sender, subject artifact, and preview. Each row
|
|
231
|
+
Lists pings with sender, subject artifact, and preview. Each ping row
|
|
232
232
|
shows a ping-id you can pass to 'codiedev read'.
|
|
233
233
|
|
|
234
|
+
Also lists feed posts and comments where a teammate @-mentioned you.
|
|
235
|
+
Those rows end in a portal link — the thread lives in the feed, so
|
|
236
|
+
read and reply to it there.
|
|
237
|
+
|
|
234
238
|
Unread rows show as ● ; read rows as · .
|
|
235
239
|
|
|
236
240
|
Examples:
|
package/dist/commands/inbox.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CodiedevConfig } from "../utils";
|
|
2
|
+
export declare function runInbox(args: string[], configOverride?: CodiedevConfig): Promise<void>;
|
|
2
3
|
export declare function runRead(args: string[]): Promise<void>;
|
package/dist/commands/inbox.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runInbox = runInbox;
|
|
4
4
|
exports.runRead = runRead;
|
|
5
5
|
const shared_1 = require("./shared");
|
|
6
|
+
const inboxFormat_1 = require("../inboxFormat");
|
|
6
7
|
function parseInboxArgs(args) {
|
|
7
8
|
let unreadOnly = false;
|
|
8
9
|
let limit;
|
|
@@ -23,9 +24,9 @@ function parseInboxArgs(args) {
|
|
|
23
24
|
}
|
|
24
25
|
return { unreadOnly, limit };
|
|
25
26
|
}
|
|
26
|
-
async function runInbox(args) {
|
|
27
|
+
async function runInbox(args, configOverride) {
|
|
27
28
|
const { unreadOnly, limit } = parseInboxArgs(args);
|
|
28
|
-
const config = (0, shared_1.requireConfig)();
|
|
29
|
+
const config = configOverride ?? (0, shared_1.requireConfig)();
|
|
29
30
|
const start = Date.now();
|
|
30
31
|
try {
|
|
31
32
|
const res = await (0, shared_1.apiRequest)("GET", "/api/cli/inbox", {
|
|
@@ -40,26 +41,14 @@ async function runInbox(args) {
|
|
|
40
41
|
ok: true,
|
|
41
42
|
latencyMs: Date.now() - start,
|
|
42
43
|
});
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log();
|
|
51
|
-
for (const p of pings) {
|
|
52
|
-
const dot = p.readAt ? "·" : "●";
|
|
53
|
-
const from = p.from?.name ?? "unknown";
|
|
54
|
-
const when = (0, shared_1.timeAgo)(p.createdAt);
|
|
55
|
-
const where = p.subjectKey ? ` · on ${p.subjectKey}` : "";
|
|
56
|
-
const preview = p.preview || "(no body)";
|
|
57
|
-
console.log(`${dot} ${from}${where} · ${when}`);
|
|
58
|
-
console.log(` ${preview}`);
|
|
59
|
-
console.log(` id: ${p.pingId}`);
|
|
60
|
-
console.log();
|
|
44
|
+
for (const line of (0, inboxFormat_1.formatInbox)({
|
|
45
|
+
pings: res.pings,
|
|
46
|
+
mentions: res.mentions,
|
|
47
|
+
unreadOnly,
|
|
48
|
+
limit,
|
|
49
|
+
})) {
|
|
50
|
+
console.log(line);
|
|
61
51
|
}
|
|
62
|
-
console.log(`Mark read: codiedev read <ping-id>`);
|
|
63
52
|
}
|
|
64
53
|
catch (err) {
|
|
65
54
|
(0, shared_1.recordAgentEvent)(config, {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface InboxPing {
|
|
2
|
+
pingId: string;
|
|
3
|
+
createdAt: number;
|
|
4
|
+
readAt?: number;
|
|
5
|
+
from: {
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
} | null;
|
|
9
|
+
subjectKey: string | null;
|
|
10
|
+
subject: {
|
|
11
|
+
title?: string;
|
|
12
|
+
} | null;
|
|
13
|
+
preview: string;
|
|
14
|
+
}
|
|
15
|
+
export interface InboxMention {
|
|
16
|
+
mentionId: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
readAt?: number | null;
|
|
19
|
+
from: {
|
|
20
|
+
name: string;
|
|
21
|
+
email: string;
|
|
22
|
+
} | null;
|
|
23
|
+
postTitle: string | null;
|
|
24
|
+
preview: string | null;
|
|
25
|
+
url: string | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render the inbox for BOTH surfaces — `codiedev inbox` and the MCP
|
|
29
|
+
* `codiedev_inbox` tool an agent calls for "any messages?". Shared so the
|
|
30
|
+
* agent path can never drift back to pings-only while the human path shows
|
|
31
|
+
* mentions.
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatInbox(opts: {
|
|
34
|
+
pings: InboxPing[];
|
|
35
|
+
mentions?: InboxMention[];
|
|
36
|
+
unreadOnly: boolean;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): string[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatInbox = formatInbox;
|
|
4
|
+
const shared_1 = require("./commands/shared");
|
|
5
|
+
/**
|
|
6
|
+
* Render the inbox for BOTH surfaces — `codiedev inbox` and the MCP
|
|
7
|
+
* `codiedev_inbox` tool an agent calls for "any messages?". Shared so the
|
|
8
|
+
* agent path can never drift back to pings-only while the human path shows
|
|
9
|
+
* mentions.
|
|
10
|
+
*/
|
|
11
|
+
function formatInbox(opts) {
|
|
12
|
+
const mentions = opts.mentions ?? [];
|
|
13
|
+
// The backend applies `limit` to each source independently, so asking for N
|
|
14
|
+
// can return up to 2N — and with no `--limit` it defaults to 50 per source,
|
|
15
|
+
// i.e. 100 rows, twice the pre-change maximum. Cap the COMBINED list by
|
|
16
|
+
// recency, always (matching the backend's own default), so the header is
|
|
17
|
+
// truthful and `--limit` actually holds.
|
|
18
|
+
//
|
|
19
|
+
// Selected by identity rather than by a `createdAt >= cutoff` filter: rows
|
|
20
|
+
// tied on the cutoff millisecond are reachable (Convex freezes Date.now()
|
|
21
|
+
// per transaction, and seeded activity carries synthetic stamps) and a
|
|
22
|
+
// threshold filter lets every tied row through, overshooting the limit.
|
|
23
|
+
const DEFAULT_LIMIT = 50;
|
|
24
|
+
const cap = opts.limit ?? DEFAULT_LIMIT;
|
|
25
|
+
// Pings claim the budget first, then mentions fill whatever is left — both
|
|
26
|
+
// newest-first. Before this change pings owned the whole budget, so this is
|
|
27
|
+
// purely additive and can never hide a ping that used to be visible. A flat
|
|
28
|
+
// recency cap would instead let a busy feed evict every direct ping, and
|
|
29
|
+
// pings are the actionable half: they carry the id you reply to, and losing
|
|
30
|
+
// them all takes the mark-read footer with them.
|
|
31
|
+
const byRecency = (xs) => [...xs].sort((a, b) => b.createdAt - a.createdAt);
|
|
32
|
+
const keptPings = byRecency(opts.pings).slice(0, cap);
|
|
33
|
+
const keptMentions = byRecency(mentions).slice(0, Math.max(0, cap - keptPings.length));
|
|
34
|
+
const total = keptPings.length + keptMentions.length;
|
|
35
|
+
if (total === 0) {
|
|
36
|
+
return [opts.unreadOnly ? "No unread messages." : "Inbox is empty."];
|
|
37
|
+
}
|
|
38
|
+
const unreadCount = keptPings.filter((p) => !p.readAt).length +
|
|
39
|
+
keptMentions.filter((m) => !m.readAt).length;
|
|
40
|
+
const lines = [
|
|
41
|
+
`Inbox · ${total} ${opts.unreadOnly ? "unread" : "recent"}${opts.unreadOnly ? "" : ` · ${unreadCount} unread`}`,
|
|
42
|
+
"",
|
|
43
|
+
];
|
|
44
|
+
for (const p of keptPings) {
|
|
45
|
+
const dot = p.readAt ? "·" : "●";
|
|
46
|
+
const from = p.from?.name ?? "unknown";
|
|
47
|
+
const where = p.subjectKey ? ` · on ${p.subjectKey}` : "";
|
|
48
|
+
lines.push(`${dot} ${from}${where} · ${(0, shared_1.timeAgo)(p.createdAt)}`);
|
|
49
|
+
lines.push(` ${p.preview || "(no body)"}`);
|
|
50
|
+
lines.push(` id: ${p.pingId}`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
}
|
|
53
|
+
// Mentions carry no CLI-side body to reply to — the thread lives in the feed
|
|
54
|
+
// — so each row ends in the deep link rather than an id.
|
|
55
|
+
for (const m of keptMentions) {
|
|
56
|
+
const dot = m.readAt ? "·" : "●";
|
|
57
|
+
const from = m.from?.name ?? "someone";
|
|
58
|
+
const where = m.postTitle ? ` · on “${m.postTitle}”` : "";
|
|
59
|
+
lines.push(`${dot} ${from} mentioned you${where} · ${(0, shared_1.timeAgo)(m.createdAt)}`);
|
|
60
|
+
if (m.preview)
|
|
61
|
+
lines.push(` ${m.preview}`);
|
|
62
|
+
if (m.url)
|
|
63
|
+
lines.push(` ${m.url}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
}
|
|
66
|
+
if (keptPings.length > 0)
|
|
67
|
+
lines.push("Mark read: codiedev read <ping-id>");
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -51,6 +51,7 @@ const fs = __importStar(require("fs"));
|
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
52
|
const utils_1 = require("./utils");
|
|
53
53
|
const shared_1 = require("./commands/shared");
|
|
54
|
+
const inboxFormat_1 = require("./inboxFormat");
|
|
54
55
|
const PKG_NAME = "codiedev";
|
|
55
56
|
// Read version from the bundled package.json so the startup log + tool reports
|
|
56
57
|
// always reflect the actually-installed version, rather than a stale constant.
|
|
@@ -748,33 +749,16 @@ async function handleInbox(args, config) {
|
|
|
748
749
|
config,
|
|
749
750
|
query: { unreadOnly: unreadOnly ? "1" : undefined, limit },
|
|
750
751
|
});
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
const unreadCount = res.pings.filter((p) => !p.readAt).length;
|
|
762
|
-
const lines = [
|
|
763
|
-
`Inbox · ${res.pings.length} ${unreadOnly ? "unread" : "recent"}${unreadOnly ? "" : ` · ${unreadCount} unread`}`,
|
|
764
|
-
"",
|
|
765
|
-
];
|
|
766
|
-
for (const p of res.pings) {
|
|
767
|
-
const status = p.readAt ? "·" : "●";
|
|
768
|
-
const from = p.from?.name ?? "unknown";
|
|
769
|
-
const where = p.subjectKey ? ` · on ${p.subjectKey}` : "";
|
|
770
|
-
lines.push(`${status} ${from}${where} · ${(0, shared_1.timeAgo)(p.createdAt)}`);
|
|
771
|
-
lines.push(` ${p.preview || "(no body)"}`);
|
|
772
|
-
lines.push(` id: ${p.pingId}`);
|
|
773
|
-
lines.push("");
|
|
774
|
-
}
|
|
775
|
-
return {
|
|
776
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
777
|
-
};
|
|
752
|
+
// Shared with `codiedev inbox` so this agent-facing path can never drift
|
|
753
|
+
// back to pings-only. CLAUDE.md routes "any messages?" here, so a
|
|
754
|
+
// mentions-blind version reproduces the bug for agents.
|
|
755
|
+
const text = (0, inboxFormat_1.formatInbox)({
|
|
756
|
+
pings: res.pings,
|
|
757
|
+
mentions: res.mentions,
|
|
758
|
+
unreadOnly,
|
|
759
|
+
limit,
|
|
760
|
+
}).join("\n");
|
|
761
|
+
return { content: [{ type: "text", text }] };
|
|
778
762
|
}
|
|
779
763
|
async function handleNote(args, config) {
|
|
780
764
|
const body = asString(args.body);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.33",
|
|
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",
|