codiedev 0.9.32 → 0.9.34
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.js +5 -1
- package/dist/codie.js +9 -0
- 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/dist/scope.d.ts +8 -0
- package/dist/scope.js +15 -0
- package/dist/security.d.ts +34 -0
- package/dist/security.js +119 -0
- package/dist/upgradeNudge.d.ts +17 -0
- package/dist/upgradeNudge.js +189 -0
- package/dist/versionCheck.d.ts +40 -0
- package/dist/versionCheck.js +68 -0
- package/package.json +1 -1
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/codie.js
CHANGED
|
@@ -53,6 +53,7 @@ const configDir_1 = require("./configDir");
|
|
|
53
53
|
const codieLauncher_1 = require("./codieLauncher");
|
|
54
54
|
const codieRuntime_1 = require("./codieRuntime");
|
|
55
55
|
const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
|
|
56
|
+
const upgradeNudge_1 = require("./upgradeNudge");
|
|
56
57
|
function git(args) {
|
|
57
58
|
const result = (0, child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
58
59
|
return result.status === 0 ? result.stdout.trim() : "";
|
|
@@ -177,6 +178,14 @@ async function main() {
|
|
|
177
178
|
if (!key) {
|
|
178
179
|
throw new Error("Connect your Codiedev account first with `codiedev connect`");
|
|
179
180
|
}
|
|
181
|
+
// Before anything paints: once the OpenCode engine owns stdio, a prompt is
|
|
182
|
+
// unreadable. A successful in-place upgrade relaunches this command with the
|
|
183
|
+
// new package, so the stale process must stop here.
|
|
184
|
+
const update = await (0, upgradeNudge_1.maybeOfferUpgrade)();
|
|
185
|
+
if (update.relaunched) {
|
|
186
|
+
process.exitCode = update.status ?? 0;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
180
189
|
const gatewayUrl = process.env.CODIEDEV_GATEWAY_URL || "https://gateway.codiedev.com/api/v1";
|
|
181
190
|
const catalog = await (0, codieLauncher_1.fetchApprovedCatalog)({ baseUrl: gatewayUrl, key });
|
|
182
191
|
const parsed = (0, codieLauncher_1.parseCodieArgs)(process.argv.slice(2));
|
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/dist/scope.d.ts
ADDED
package/dist/scope.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatScope = formatScope;
|
|
4
|
+
exports.printScope = printScope;
|
|
5
|
+
function formatScope(info) {
|
|
6
|
+
if (!info.workspaceName)
|
|
7
|
+
return "→ unknown workspace";
|
|
8
|
+
if (info.matchedRepo) {
|
|
9
|
+
return `→ ${info.workspaceName} (matched ${info.matchedRepo.fullName})`;
|
|
10
|
+
}
|
|
11
|
+
return `→ ${info.workspaceName} (default profile, no repo match)`;
|
|
12
|
+
}
|
|
13
|
+
function printScope(info) {
|
|
14
|
+
console.log(formatScope(info));
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type SecurityHookPhase = "pre" | "post" | "failure";
|
|
2
|
+
export type AgentSurface = "claude-code" | "codex";
|
|
3
|
+
export interface RawSecurityHookInput {
|
|
4
|
+
session_id?: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
hookEventName?: string;
|
|
9
|
+
tool_name?: string;
|
|
10
|
+
toolName?: string;
|
|
11
|
+
tool_input?: unknown;
|
|
12
|
+
toolInput?: unknown;
|
|
13
|
+
tool_response?: unknown;
|
|
14
|
+
toolResponse?: unknown;
|
|
15
|
+
tool_use_id?: string;
|
|
16
|
+
toolUseId?: string;
|
|
17
|
+
duration_ms?: number;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface SecurityWorkflowEvent {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
source: AgentSurface;
|
|
24
|
+
phase: SecurityHookPhase;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
toolName: string;
|
|
27
|
+
toolUseId?: string;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
observedAt: number;
|
|
30
|
+
toolDurationMs?: number;
|
|
31
|
+
payload: unknown;
|
|
32
|
+
redactionCount: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function buildSecurityWorkflowEvent(input: RawSecurityHookInput, phase: SecurityHookPhase, source: AgentSurface, now?: () => number): SecurityWorkflowEvent | null;
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildSecurityWorkflowEvent = buildSecurityWorkflowEvent;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const SECRET_KEY = /(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|passwd|secret|private[_-]?key|client[_-]?secret)/i;
|
|
39
|
+
const SECRET_VALUE_PATTERNS = [
|
|
40
|
+
/\b(?:sk|rk|pk)-(?:live|test|proj)?[-_A-Za-z0-9]{16,}\b/g,
|
|
41
|
+
/\bgh[opusr]_[A-Za-z0-9]{20,}\b/g,
|
|
42
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
43
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
44
|
+
/\bBearer\s+[A-Za-z0-9._~+\/-]{16,}=*\b/gi,
|
|
45
|
+
];
|
|
46
|
+
function fingerprint(value) {
|
|
47
|
+
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
48
|
+
}
|
|
49
|
+
function redactString(value) {
|
|
50
|
+
let count = 0;
|
|
51
|
+
let redacted = value;
|
|
52
|
+
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
53
|
+
redacted = redacted.replace(pattern, (match) => {
|
|
54
|
+
count += 1;
|
|
55
|
+
return `[REDACTED_SECRET:${fingerprint(match)}]`;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Keep hook payloads bounded. Full transcripts already use Codie's existing
|
|
59
|
+
// capture path; the detector only needs immediate causal context here.
|
|
60
|
+
if (redacted.length > 16_384)
|
|
61
|
+
redacted = `${redacted.slice(0, 16_384)}[TRUNCATED]`;
|
|
62
|
+
return { value: redacted, count };
|
|
63
|
+
}
|
|
64
|
+
function redact(value, key = "", depth = 0) {
|
|
65
|
+
if (depth > 8)
|
|
66
|
+
return { value: "[TRUNCATED_DEPTH]", count: 0 };
|
|
67
|
+
if (SECRET_KEY.test(key) && value !== null && value !== undefined) {
|
|
68
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
69
|
+
return { value: `[REDACTED_SECRET:${fingerprint(text)}]`, count: 1 };
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === "string")
|
|
72
|
+
return redactString(value);
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
let count = 0;
|
|
75
|
+
const next = value.slice(0, 200).map((item) => {
|
|
76
|
+
const result = redact(item, key, depth + 1);
|
|
77
|
+
count += result.count;
|
|
78
|
+
return result.value;
|
|
79
|
+
});
|
|
80
|
+
return { value: next, count };
|
|
81
|
+
}
|
|
82
|
+
if (value && typeof value === "object") {
|
|
83
|
+
let count = 0;
|
|
84
|
+
const next = {};
|
|
85
|
+
for (const [childKey, childValue] of Object.entries(value).slice(0, 200)) {
|
|
86
|
+
const result = redact(childValue, childKey, depth + 1);
|
|
87
|
+
count += result.count;
|
|
88
|
+
next[childKey] = result.value;
|
|
89
|
+
}
|
|
90
|
+
return { value: next, count };
|
|
91
|
+
}
|
|
92
|
+
return { value, count: 0 };
|
|
93
|
+
}
|
|
94
|
+
function buildSecurityWorkflowEvent(input, phase, source, now = Date.now) {
|
|
95
|
+
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
96
|
+
const toolName = input.tool_name ?? input.toolName ?? "";
|
|
97
|
+
if (!sessionId || !toolName)
|
|
98
|
+
return null;
|
|
99
|
+
const rawPayload = {
|
|
100
|
+
input: input.tool_input ?? input.toolInput,
|
|
101
|
+
...(phase === "post" ? { response: input.tool_response ?? input.toolResponse } : {}),
|
|
102
|
+
...(phase === "failure" ? { error: input.error ?? input.tool_response ?? input.toolResponse } : {}),
|
|
103
|
+
};
|
|
104
|
+
const sanitized = redact(rawPayload);
|
|
105
|
+
const duration = input.duration_ms ?? input.durationMs;
|
|
106
|
+
return {
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
source,
|
|
109
|
+
phase,
|
|
110
|
+
sessionId,
|
|
111
|
+
toolName,
|
|
112
|
+
toolUseId: input.tool_use_id ?? input.toolUseId,
|
|
113
|
+
cwd: input.cwd,
|
|
114
|
+
observedAt: now(),
|
|
115
|
+
toolDurationMs: typeof duration === "number" && Number.isFinite(duration) ? duration : undefined,
|
|
116
|
+
payload: sanitized.value,
|
|
117
|
+
redactionCount: sanitized.count,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/upgradeNudge.d.ts
CHANGED
|
@@ -5,3 +5,20 @@
|
|
|
5
5
|
* this feature existed.
|
|
6
6
|
*/
|
|
7
7
|
export declare function maybePrintUpgradeNudge(): Promise<void>;
|
|
8
|
+
export interface OfferOpts {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
argv?: string[];
|
|
11
|
+
nowMs?: number;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
log?: (line: string) => void;
|
|
14
|
+
isTty?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Prompt-capable sibling of maybePrintUpgradeNudge. Call this before handing
|
|
18
|
+
* the terminal to a TUI. A successful upgrade relaunches the fresh CLI and
|
|
19
|
+
* tells the stale caller to stop rather than continuing with old modules.
|
|
20
|
+
*/
|
|
21
|
+
export declare function maybeOfferUpgrade(opts?: OfferOpts): Promise<{
|
|
22
|
+
relaunched: boolean;
|
|
23
|
+
status?: number;
|
|
24
|
+
}>;
|
package/dist/upgradeNudge.js
CHANGED
|
@@ -43,8 +43,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.maybePrintUpgradeNudge = maybePrintUpgradeNudge;
|
|
46
|
+
exports.maybeOfferUpgrade = maybeOfferUpgrade;
|
|
46
47
|
const fs = __importStar(require("fs"));
|
|
47
48
|
const path = __importStar(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
48
50
|
const version_1 = require("./version");
|
|
49
51
|
const versionCheck_1 = require("./versionCheck");
|
|
50
52
|
const configDir_1 = require("./configDir");
|
|
@@ -122,3 +124,190 @@ async function maybePrintUpgradeNudge() {
|
|
|
122
124
|
console.log("");
|
|
123
125
|
}
|
|
124
126
|
}
|
|
127
|
+
const OFFER_CACHE_NAME = ".update-offer-cache.json";
|
|
128
|
+
// This runs before the harness paints, so the timeout is startup latency the
|
|
129
|
+
// user feels. A missed check costs nothing; the next stale-cache launch tries
|
|
130
|
+
// again.
|
|
131
|
+
const OFFER_FETCH_TIMEOUT_MS = 1500;
|
|
132
|
+
const UPGRADE_TIMEOUT_MS = 120_000;
|
|
133
|
+
function offerCachePath(env) {
|
|
134
|
+
const base = env.CODIEDEV_CONFIG_DIR?.trim()
|
|
135
|
+
? path.resolve(env.CODIEDEV_CONFIG_DIR.trim())
|
|
136
|
+
: (0, configDir_1.codiedevStateDir)();
|
|
137
|
+
return path.join(base, OFFER_CACHE_NAME);
|
|
138
|
+
}
|
|
139
|
+
function readOfferCache(env) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(fs.readFileSync(offerCachePath(env), "utf8"));
|
|
142
|
+
if (typeof parsed.fetchedAt !== "number")
|
|
143
|
+
return null;
|
|
144
|
+
return {
|
|
145
|
+
latest: typeof parsed.latest === "string" ? parsed.latest : null,
|
|
146
|
+
fetchedAt: parsed.fetchedAt,
|
|
147
|
+
promptedAt: typeof parsed.promptedAt === "number" ? parsed.promptedAt : 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// If this cannot be persisted, the whole feature stays off. Without memory,
|
|
155
|
+
// every launch would refetch and re-nag on ephemeral or read-only filesystems.
|
|
156
|
+
function writeOfferCache(entry, env) {
|
|
157
|
+
try {
|
|
158
|
+
const target = offerCachePath(env);
|
|
159
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
160
|
+
fs.writeFileSync(target, JSON.stringify(entry), "utf8");
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function isCiEnvironment(env) {
|
|
168
|
+
return Boolean(env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.BUILD_NUMBER);
|
|
169
|
+
}
|
|
170
|
+
async function fetchLatest(fetchImpl) {
|
|
171
|
+
try {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timer = setTimeout(() => controller.abort(), OFFER_FETCH_TIMEOUT_MS);
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetchImpl(REGISTRY_URL, {
|
|
176
|
+
headers: { Accept: "application/json" },
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
return null;
|
|
181
|
+
const json = (await res.json());
|
|
182
|
+
return typeof json.version === "string" ? json.version : null;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Single-keypress y/n prompt. EOF, Ctrl-C, lost raw mode, and tty errors all
|
|
193
|
+
// default to no so update handling can never strand the launcher.
|
|
194
|
+
function promptYesNo(question) {
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
const stdin = process.stdin;
|
|
197
|
+
process.stdout.write(question);
|
|
198
|
+
const wasRaw = stdin.isRaw;
|
|
199
|
+
try {
|
|
200
|
+
stdin.setRawMode(true);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
process.stdout.write("\n");
|
|
204
|
+
resolve(false);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
stdin.resume();
|
|
208
|
+
let settled = false;
|
|
209
|
+
const settle = (yes) => {
|
|
210
|
+
if (settled)
|
|
211
|
+
return;
|
|
212
|
+
settled = true;
|
|
213
|
+
stdin.removeListener("data", onData);
|
|
214
|
+
stdin.removeListener("end", onGone);
|
|
215
|
+
stdin.removeListener("error", onGone);
|
|
216
|
+
try {
|
|
217
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Best-effort; the engine resets the tty when it starts.
|
|
221
|
+
}
|
|
222
|
+
stdin.pause();
|
|
223
|
+
process.stdout.write(`${yes ? "y" : "n"}\n`);
|
|
224
|
+
resolve(yes);
|
|
225
|
+
};
|
|
226
|
+
const onData = (chunk) => settle(String(chunk).trim().toLowerCase() === "y");
|
|
227
|
+
const onGone = () => settle(false);
|
|
228
|
+
stdin.once("data", onData);
|
|
229
|
+
stdin.once("end", onGone);
|
|
230
|
+
stdin.once("error", onGone);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function runUpgrade() {
|
|
234
|
+
const result = (0, child_process_1.spawnSync)("npm", ["i", "-g", "codiedev@latest"], {
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
timeout: UPGRADE_TIMEOUT_MS,
|
|
237
|
+
shell: process.platform === "win32",
|
|
238
|
+
});
|
|
239
|
+
return !result.error && result.status === 0;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Prompt-capable sibling of maybePrintUpgradeNudge. Call this before handing
|
|
243
|
+
* the terminal to a TUI. A successful upgrade relaunches the fresh CLI and
|
|
244
|
+
* tells the stale caller to stop rather than continuing with old modules.
|
|
245
|
+
*/
|
|
246
|
+
async function maybeOfferUpgrade(opts = {}) {
|
|
247
|
+
const env = opts.env ?? process.env;
|
|
248
|
+
const argv = opts.argv ?? process.argv;
|
|
249
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
250
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
251
|
+
const log = opts.log ?? ((line) => console.error(line));
|
|
252
|
+
if (env.CODIEDEV_NO_UPDATE_CHECK)
|
|
253
|
+
return { relaunched: false };
|
|
254
|
+
const cache = readOfferCache(env);
|
|
255
|
+
let latest = cache?.latest ?? null;
|
|
256
|
+
let fetchedAt = cache?.fetchedAt ?? 0;
|
|
257
|
+
const promptedAt = cache?.promptedAt ?? 0;
|
|
258
|
+
if (!cache || nowMs - cache.fetchedAt > CACHE_TTL_MS) {
|
|
259
|
+
// Probe writability before paying the network cost.
|
|
260
|
+
if (!writeOfferCache(cache ?? { latest: null, fetchedAt: 0, promptedAt: 0 }, env)) {
|
|
261
|
+
return { relaunched: false };
|
|
262
|
+
}
|
|
263
|
+
latest = await fetchLatest(fetchImpl);
|
|
264
|
+
fetchedAt = nowMs;
|
|
265
|
+
if (!writeOfferCache({ latest, fetchedAt, promptedAt }, env)) {
|
|
266
|
+
return { relaunched: false };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const installKind = (0, versionCheck_1.resolveInstallKind)({
|
|
270
|
+
dirname: __dirname,
|
|
271
|
+
env,
|
|
272
|
+
existsSync: fs.existsSync,
|
|
273
|
+
readFileSync: fs.readFileSync,
|
|
274
|
+
accessSync: fs.accessSync,
|
|
275
|
+
});
|
|
276
|
+
const { action, command } = (0, versionCheck_1.decideUpdateAction)({
|
|
277
|
+
current: version_1.CLI_VERSION,
|
|
278
|
+
latest,
|
|
279
|
+
installKind,
|
|
280
|
+
isTty: opts.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
281
|
+
isCi: isCiEnvironment(env),
|
|
282
|
+
promptedAt,
|
|
283
|
+
nowMs,
|
|
284
|
+
});
|
|
285
|
+
if (action === "none")
|
|
286
|
+
return { relaunched: false };
|
|
287
|
+
log("");
|
|
288
|
+
log(`\x1b[33mcodiedev ${latest} is available\x1b[0m (you're on ${version_1.CLI_VERSION})`);
|
|
289
|
+
if (action === "notify") {
|
|
290
|
+
log(` Upgrade: \x1b[36m${command}\x1b[0m`);
|
|
291
|
+
log("");
|
|
292
|
+
return { relaunched: false };
|
|
293
|
+
}
|
|
294
|
+
writeOfferCache({ latest, fetchedAt, promptedAt: nowMs }, env);
|
|
295
|
+
if (!(await promptYesNo(" Update now? [y/N] "))) {
|
|
296
|
+
log(` Skipping. Upgrade later with \x1b[36m${command}\x1b[0m`);
|
|
297
|
+
log("");
|
|
298
|
+
return { relaunched: false };
|
|
299
|
+
}
|
|
300
|
+
if (!runUpgrade()) {
|
|
301
|
+
log(`\x1b[33mcodiedev: upgrade failed — continuing on ${version_1.CLI_VERSION}\x1b[0m`);
|
|
302
|
+
log(` Try it by hand: \x1b[36m${command}\x1b[0m`);
|
|
303
|
+
log("");
|
|
304
|
+
return { relaunched: false };
|
|
305
|
+
}
|
|
306
|
+
const relaunch = (0, child_process_1.spawnSync)(process.execPath, [argv[1], ...argv.slice(2)], {
|
|
307
|
+
stdio: "inherit",
|
|
308
|
+
env: { ...env, CODIEDEV_NO_UPDATE_CHECK: "1" },
|
|
309
|
+
});
|
|
310
|
+
if (relaunch.error)
|
|
311
|
+
return { relaunched: false };
|
|
312
|
+
return { relaunched: true, status: relaunch.status === null ? 1 : relaunch.status };
|
|
313
|
+
}
|
package/dist/versionCheck.d.ts
CHANGED
|
@@ -26,3 +26,43 @@ export interface UpdateWarningInput {
|
|
|
26
26
|
* as-given and answers a single question — should the user see a nag?
|
|
27
27
|
*/
|
|
28
28
|
export declare function shouldShowUpdateWarning(input: UpdateWarningInput): boolean;
|
|
29
|
+
export type InstallKind = "npx" | "container" | "global" | "readonly";
|
|
30
|
+
export interface InstallKindDeps {
|
|
31
|
+
dirname: string;
|
|
32
|
+
env: NodeJS.ProcessEnv;
|
|
33
|
+
existsSync: (p: string) => boolean;
|
|
34
|
+
readFileSync: (p: string, enc: string) => string;
|
|
35
|
+
accessSync: (p: string, mode?: number) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* npx — running from npm's _npx cache; nothing to upgrade in place.
|
|
39
|
+
* container — baked into an image; the honest fix is rebuilding it.
|
|
40
|
+
* global — a real install directory we can write to. Upgradable.
|
|
41
|
+
* readonly — a real install directory we cannot write to (root-owned global
|
|
42
|
+
* on a shared box, read-only mount). Needs sudo, so print rather
|
|
43
|
+
* than attempt an upgrade that would EACCES.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveInstallKind(deps: InstallKindDeps): InstallKind;
|
|
46
|
+
export declare function upgradeCommand(kind: InstallKind, pkg?: string): string;
|
|
47
|
+
export type UpdateAction = "none" | "notify" | "prompt";
|
|
48
|
+
export interface UpdateActionInput {
|
|
49
|
+
current: string;
|
|
50
|
+
latest: string | null;
|
|
51
|
+
installKind: InstallKind;
|
|
52
|
+
isTty: boolean;
|
|
53
|
+
isCi: boolean;
|
|
54
|
+
promptedAt?: number;
|
|
55
|
+
nowMs: number;
|
|
56
|
+
cacheTtlMs?: number;
|
|
57
|
+
pkg?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `promptedAt` makes a decline stick until the next window instead of asking
|
|
61
|
+
* on every launch. It is deliberately not keyed to a dismissed version:
|
|
62
|
+
* somebody who says no today should be asked again tomorrow, not silently
|
|
63
|
+
* stranded on that version forever.
|
|
64
|
+
*/
|
|
65
|
+
export declare function decideUpdateAction(input: UpdateActionInput): {
|
|
66
|
+
action: UpdateAction;
|
|
67
|
+
command: string;
|
|
68
|
+
};
|
package/dist/versionCheck.js
CHANGED
|
@@ -7,6 +7,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.parseSemver = parseSemver;
|
|
8
8
|
exports.compareSemver = compareSemver;
|
|
9
9
|
exports.shouldShowUpdateWarning = shouldShowUpdateWarning;
|
|
10
|
+
exports.resolveInstallKind = resolveInstallKind;
|
|
11
|
+
exports.upgradeCommand = upgradeCommand;
|
|
12
|
+
exports.decideUpdateAction = decideUpdateAction;
|
|
10
13
|
function parseSemver(input) {
|
|
11
14
|
if (!input)
|
|
12
15
|
return null;
|
|
@@ -47,3 +50,68 @@ function shouldShowUpdateWarning(input) {
|
|
|
47
50
|
return false;
|
|
48
51
|
return compareSemver(input.current, input.latest) === -1;
|
|
49
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* npx — running from npm's _npx cache; nothing to upgrade in place.
|
|
55
|
+
* container — baked into an image; the honest fix is rebuilding it.
|
|
56
|
+
* global — a real install directory we can write to. Upgradable.
|
|
57
|
+
* readonly — a real install directory we cannot write to (root-owned global
|
|
58
|
+
* on a shared box, read-only mount). Needs sudo, so print rather
|
|
59
|
+
* than attempt an upgrade that would EACCES.
|
|
60
|
+
*/
|
|
61
|
+
function resolveInstallKind(deps) {
|
|
62
|
+
if (/[\\/](?:_npx|\.npm[\\/]_npx)[\\/]/.test(deps.dirname))
|
|
63
|
+
return "npx";
|
|
64
|
+
if (deps.env.KUBERNETES_SERVICE_HOST)
|
|
65
|
+
return "container";
|
|
66
|
+
if (deps.existsSync("/.dockerenv"))
|
|
67
|
+
return "container";
|
|
68
|
+
try {
|
|
69
|
+
if (/docker|containerd|kubepods|lxc/.test(deps.readFileSync("/proc/1/cgroup", "utf8"))) {
|
|
70
|
+
return "container";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// not Linux, or no procfs — fall through to the writability probe
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
// 2 = fs.constants.W_OK, inlined so this module stays fs-free.
|
|
78
|
+
deps.accessSync(deps.dirname, 2);
|
|
79
|
+
return "global";
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "readonly";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function upgradeCommand(kind, pkg = "codiedev") {
|
|
86
|
+
switch (kind) {
|
|
87
|
+
case "npx":
|
|
88
|
+
return `npx --package ${pkg}@latest codie`;
|
|
89
|
+
case "container":
|
|
90
|
+
return `rebuild your image (npm i -g ${pkg}@latest)`;
|
|
91
|
+
case "readonly":
|
|
92
|
+
return `sudo npm i -g ${pkg}@latest`;
|
|
93
|
+
default:
|
|
94
|
+
return `npm i -g ${pkg}@latest`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `promptedAt` makes a decline stick until the next window instead of asking
|
|
99
|
+
* on every launch. It is deliberately not keyed to a dismissed version:
|
|
100
|
+
* somebody who says no today should be asked again tomorrow, not silently
|
|
101
|
+
* stranded on that version forever.
|
|
102
|
+
*/
|
|
103
|
+
function decideUpdateAction(input) {
|
|
104
|
+
const command = upgradeCommand(input.installKind, input.pkg);
|
|
105
|
+
const ttl = input.cacheTtlMs ?? 24 * 60 * 60 * 1000;
|
|
106
|
+
if (!input.latest || compareSemver(input.current, input.latest) !== -1) {
|
|
107
|
+
return { action: "none", command };
|
|
108
|
+
}
|
|
109
|
+
if (input.isCi || !input.isTty)
|
|
110
|
+
return { action: "none", command };
|
|
111
|
+
const promptedAt = input.promptedAt ?? 0;
|
|
112
|
+
const askedRecently = promptedAt > 0 && input.nowMs - promptedAt <= ttl;
|
|
113
|
+
if (input.installKind === "global" && !askedRecently) {
|
|
114
|
+
return { action: "prompt", command };
|
|
115
|
+
}
|
|
116
|
+
return { action: "notify", command };
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.34",
|
|
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",
|