codiedev 0.9.33 → 0.9.35
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/codie.js +11 -1
- package/dist/codieLauncher.d.ts +7 -0
- package/dist/codieLauncher.js +14 -0
- package/dist/hook.js +4 -0
- package/dist/upgradeNudge.d.ts +17 -0
- package/dist/upgradeNudge.js +189 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +144 -0
- package/dist/versionCheck.d.ts +40 -0
- package/dist/versionCheck.js +68 -0
- package/package.json +1 -1
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));
|
|
@@ -222,7 +231,8 @@ async function main() {
|
|
|
222
231
|
CODIEDEV_HEAD_SHA: git(["rev-parse", "HEAD"]),
|
|
223
232
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
224
233
|
};
|
|
225
|
-
const
|
|
234
|
+
const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
|
|
235
|
+
const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
|
|
226
236
|
if (result.error)
|
|
227
237
|
throw result.error;
|
|
228
238
|
process.exitCode = result.status === null ? 1 : result.status;
|
package/dist/codieLauncher.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ export interface CatalogModel {
|
|
|
2
2
|
id: string;
|
|
3
3
|
name?: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
7
|
+
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
8
|
+
* --dir, a real smoke run launched in a throwaway repo resumed the caller's
|
|
9
|
+
* main checkout and executed there. TUI mode already owns its cwd directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withCodieWorkingDirectory(args: string[], cwd: string): string[];
|
|
5
12
|
export declare function bareModel(value: unknown): string;
|
|
6
13
|
export declare function buildCodieConfig({ existing, catalog, env, plugin, plugins, }: {
|
|
7
14
|
existing?: Record<string, any>;
|
package/dist/codieLauncher.js
CHANGED
|
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
|
|
46
|
+
exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
|
|
46
47
|
exports.bareModel = bareModel;
|
|
47
48
|
exports.buildCodieConfig = buildCodieConfig;
|
|
48
49
|
exports.withBrandPlugin = withBrandPlugin;
|
|
@@ -61,6 +62,19 @@ const DEFAULT_NAMES = {
|
|
|
61
62
|
"z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
|
|
62
63
|
"moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
|
|
63
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
67
|
+
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
68
|
+
* --dir, a real smoke run launched in a throwaway repo resumed the caller's
|
|
69
|
+
* main checkout and executed there. TUI mode already owns its cwd directly.
|
|
70
|
+
*/
|
|
71
|
+
function withCodieWorkingDirectory(args, cwd) {
|
|
72
|
+
if (args[0] !== "run")
|
|
73
|
+
return args;
|
|
74
|
+
if (args.some((arg) => arg === "--dir" || arg.startsWith("--dir=")))
|
|
75
|
+
return args;
|
|
76
|
+
return [...args, "--dir", cwd];
|
|
77
|
+
}
|
|
64
78
|
function cleanBaseUrl(value) {
|
|
65
79
|
return String(value || "https://gateway.codiedev.com/api/v1").replace(/\/+$/, "");
|
|
66
80
|
}
|
package/dist/hook.js
CHANGED
|
@@ -661,6 +661,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
661
661
|
// worktree's branch+sha so all the resulting PRs attribute. branch/headSha
|
|
662
662
|
// remain the primary (most-recent) values for back-compat.
|
|
663
663
|
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
664
|
+
const gitRefs = (0, utils_1.collectCaptureGitRefs)(captureDirs);
|
|
664
665
|
// Union the worktree-dir branches with the branches the agent actually
|
|
665
666
|
// committed/pushed to (parsed from the transcript's git output). Like the
|
|
666
667
|
// SHAs below, these are independent of end-of-session filesystem state, so
|
|
@@ -678,6 +679,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
678
679
|
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
679
680
|
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
680
681
|
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
682
|
+
const pullRequestUrls = (0, utils_1.parsePullRequestUrls)(transcriptContent);
|
|
681
683
|
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
682
684
|
// already holding — no extra hooks, no extra git/process calls. parseUsage
|
|
683
685
|
// keeps the literal mode (it has its own desktop parser) and reads the RAW
|
|
@@ -700,6 +702,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
700
702
|
headSha,
|
|
701
703
|
branches,
|
|
702
704
|
headShas,
|
|
705
|
+
gitRefs,
|
|
706
|
+
pullRequestUrls,
|
|
703
707
|
inputTokens: usage.inputTokens,
|
|
704
708
|
outputTokens: usage.outputTokens,
|
|
705
709
|
cacheTokens: usage.cacheTokens,
|
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/utils.d.ts
CHANGED
|
@@ -45,6 +45,13 @@ export declare function collectCaptureKeys(dirs: string[], getBranch?: (d: strin
|
|
|
45
45
|
branches: string[];
|
|
46
46
|
headShas: string[];
|
|
47
47
|
};
|
|
48
|
+
export type CaptureGitRef = {
|
|
49
|
+
repoUrl: string;
|
|
50
|
+
branch?: string;
|
|
51
|
+
headSha?: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function collectCaptureGitRefs(dirs: string[], getRemote?: (d: string) => string | null, getBranch?: (d: string) => string | null, getSha?: (d: string) => string | null): CaptureGitRef[];
|
|
54
|
+
export declare function parsePullRequestUrls(transcriptContent: string): string[];
|
|
48
55
|
export declare function extractShaTokens(transcriptContent: string): string[];
|
|
49
56
|
export declare function parseAuthoredBranches(transcriptContent: string): string[];
|
|
50
57
|
export declare function parseAuthoredShas(transcriptContent: string, repoRoot: string, resolveFullBatch?: (shas: string[]) => Map<string, string>): string[];
|
|
@@ -64,6 +71,8 @@ export interface CapturePayloadInput {
|
|
|
64
71
|
headSha?: string | null;
|
|
65
72
|
branches?: string[];
|
|
66
73
|
headShas?: string[];
|
|
74
|
+
gitRefs?: CaptureGitRef[];
|
|
75
|
+
pullRequestUrls?: string[];
|
|
67
76
|
inputTokens?: number;
|
|
68
77
|
outputTokens?: number;
|
|
69
78
|
cacheTokens?: number;
|
package/dist/utils.js
CHANGED
|
@@ -46,6 +46,8 @@ exports.matchRepo = matchRepo;
|
|
|
46
46
|
exports.resolveCaptureDir = resolveCaptureDir;
|
|
47
47
|
exports.resolveCaptureDirs = resolveCaptureDirs;
|
|
48
48
|
exports.collectCaptureKeys = collectCaptureKeys;
|
|
49
|
+
exports.collectCaptureGitRefs = collectCaptureGitRefs;
|
|
50
|
+
exports.parsePullRequestUrls = parsePullRequestUrls;
|
|
49
51
|
exports.extractShaTokens = extractShaTokens;
|
|
50
52
|
exports.parseAuthoredBranches = parseAuthoredBranches;
|
|
51
53
|
exports.parseAuthoredShas = parseAuthoredShas;
|
|
@@ -450,6 +452,144 @@ function collectCaptureKeys(dirs, getBranch = getGitBranch, getSha = getGitHeadS
|
|
|
450
452
|
}
|
|
451
453
|
return { branch: branches[0] ?? null, headSha: headShas[0] ?? null, branches, headShas };
|
|
452
454
|
}
|
|
455
|
+
function collectCaptureGitRefs(dirs, getRemote = getGitRemoteUrl, getBranch = getGitBranch, getSha = getGitHeadSha) {
|
|
456
|
+
const out = [];
|
|
457
|
+
const seen = new Set();
|
|
458
|
+
for (const dir of dirs) {
|
|
459
|
+
const repoUrl = getRemote(dir)?.trim();
|
|
460
|
+
if (!repoUrl)
|
|
461
|
+
continue;
|
|
462
|
+
const branch = getBranch(dir)?.trim() || undefined;
|
|
463
|
+
const headSha = getSha(dir)?.trim().toLowerCase() || undefined;
|
|
464
|
+
if (!branch && !headSha)
|
|
465
|
+
continue;
|
|
466
|
+
const key = `${normalizeRepoUrl(repoUrl).toLowerCase()}::${branch ?? ""}::${headSha ?? ""}`;
|
|
467
|
+
if (seen.has(key))
|
|
468
|
+
continue;
|
|
469
|
+
seen.add(key);
|
|
470
|
+
out.push({ repoUrl, ...(branch ? { branch } : {}), ...(headSha ? { headSha } : {}) });
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
// Exact PR/MR URLs are the strongest provenance the transcript can provide.
|
|
475
|
+
// Both `gh pr create` and `glab mr create` print one, and the OpenCode capture
|
|
476
|
+
// is normalized through the same parser as Claude Code. A direct URL survives
|
|
477
|
+
// branch deletion, squash/rebase, cherry-picks, and SessionEnd happening after
|
|
478
|
+
// the PR was opened.
|
|
479
|
+
function parsePullRequestUrls(transcriptContent) {
|
|
480
|
+
const out = [];
|
|
481
|
+
const createToolIds = new Set();
|
|
482
|
+
const isCreateCommand = (command) => {
|
|
483
|
+
if (typeof command !== "string")
|
|
484
|
+
return false;
|
|
485
|
+
// A combined `gh pr view || gh pr create` result is ambiguous: when the PR
|
|
486
|
+
// already exists, the VIEW half prints its URL and CREATE never runs.
|
|
487
|
+
if (/\b(?:gh\s+pr\s+(?:view|list|status)|glab\s+mr\s+(?:view|list))\b/i.test(command)) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return command.split(/&&|\|\||;|\n/).some((statement) => /^(?:(?:env\s+)?[A-Z_][A-Z0-9_]*=\S+\s+)*(?:gh\s+pr\s+create|glab\s+mr\s+create)\b/i
|
|
491
|
+
.test(statement.trim()));
|
|
492
|
+
};
|
|
493
|
+
const scanText = (text) => {
|
|
494
|
+
const normalized = text.replace(/\\\//g, "/");
|
|
495
|
+
const candidates = normalized.match(/https?:\/\/[^\s<>"']+/gi) ?? [];
|
|
496
|
+
for (const raw of candidates) {
|
|
497
|
+
const trimmed = raw.replace(/[),.;:\]}]+$/g, "");
|
|
498
|
+
try {
|
|
499
|
+
const url = new URL(trimmed);
|
|
500
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
501
|
+
const isGithubPr = /^\/.+\/pull\/\d+$/i.test(path);
|
|
502
|
+
const isGitlabMr = /^\/.+\/-\/merge_requests\/\d+$/i.test(path);
|
|
503
|
+
if (!isGithubPr && !isGitlabMr)
|
|
504
|
+
continue;
|
|
505
|
+
const canonical = `${url.protocol}//${url.host}${path}`;
|
|
506
|
+
if (!out.includes(canonical))
|
|
507
|
+
out.push(canonical);
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Malformed tool output is ignored; capture remains best-effort.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const scanContent = (value) => {
|
|
515
|
+
if (typeof value === "string") {
|
|
516
|
+
scanText(value);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!Array.isArray(value))
|
|
520
|
+
return;
|
|
521
|
+
for (const part of value) {
|
|
522
|
+
if (typeof part === "string")
|
|
523
|
+
scanText(part);
|
|
524
|
+
else if (part && typeof part === "object") {
|
|
525
|
+
const text = part.text;
|
|
526
|
+
if (typeof text === "string")
|
|
527
|
+
scanText(text);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const entries = [];
|
|
532
|
+
for (const line of transcriptContent.split("\n")) {
|
|
533
|
+
if (!line.trim())
|
|
534
|
+
continue;
|
|
535
|
+
try {
|
|
536
|
+
const entry = JSON.parse(line);
|
|
537
|
+
entries.push(entry);
|
|
538
|
+
const content = entry?.message?.content;
|
|
539
|
+
if (Array.isArray(content)) {
|
|
540
|
+
for (const block of content) {
|
|
541
|
+
if (block?.type === "tool_use"
|
|
542
|
+
&& typeof block.id === "string"
|
|
543
|
+
&& isCreateCommand(block.input?.command)) {
|
|
544
|
+
createToolIds.add(block.id);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Codex function calls carry arguments as serialized JSON.
|
|
549
|
+
if (entry?.type === "response_item"
|
|
550
|
+
&& entry?.payload?.type === "function_call"
|
|
551
|
+
&& typeof entry.payload.call_id === "string") {
|
|
552
|
+
let args = entry.payload.arguments;
|
|
553
|
+
if (typeof args === "string") {
|
|
554
|
+
try {
|
|
555
|
+
args = JSON.parse(args);
|
|
556
|
+
}
|
|
557
|
+
catch { /* leave as string */ }
|
|
558
|
+
}
|
|
559
|
+
if (isCreateCommand(args?.cmd ?? args?.command ?? args)) {
|
|
560
|
+
createToolIds.add(entry.payload.call_id);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Never regex the raw line: prompts and assistant prose can contain a PR
|
|
566
|
+
// URL, but only actual tool output is provenance.
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Second pass: a URL is proof only when it came from the result of the
|
|
570
|
+
// specific gh/glab CREATE call above. `gh pr view/list` also prints URLs and
|
|
571
|
+
// must never claim somebody else's already-existing PR for this session.
|
|
572
|
+
for (const entry of entries) {
|
|
573
|
+
const content = entry?.message?.content;
|
|
574
|
+
if (Array.isArray(content)) {
|
|
575
|
+
for (const block of content) {
|
|
576
|
+
if (block?.type === "tool_result"
|
|
577
|
+
&& typeof block.tool_use_id === "string"
|
|
578
|
+
&& createToolIds.has(block.tool_use_id)
|
|
579
|
+
&& block.is_error !== true) {
|
|
580
|
+
scanContent(block.content);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (entry?.type === "response_item"
|
|
585
|
+
&& entry?.payload?.type === "function_call_output"
|
|
586
|
+
&& typeof entry.payload.call_id === "string"
|
|
587
|
+
&& createToolIds.has(entry.payload.call_id)) {
|
|
588
|
+
scanContent(entry.payload.output);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
453
593
|
// Pull the commit SHAs the agent actually authored/pushed straight out of the
|
|
454
594
|
// transcript's git command OUTPUT — independent of end-of-session filesystem
|
|
455
595
|
// state. Catches: commit output "[branch 4c4e790]", push ranges
|
|
@@ -690,6 +830,10 @@ function buildCapturePayload(input) {
|
|
|
690
830
|
// see no change; newer servers index the session under every key.
|
|
691
831
|
branches: input.branches && input.branches.length ? input.branches : undefined,
|
|
692
832
|
headShas: input.headShas && input.headShas.length ? input.headShas : undefined,
|
|
833
|
+
gitRefs: input.gitRefs && input.gitRefs.length ? input.gitRefs : undefined,
|
|
834
|
+
pullRequestUrls: input.pullRequestUrls && input.pullRequestUrls.length
|
|
835
|
+
? input.pullRequestUrls
|
|
836
|
+
: undefined,
|
|
693
837
|
transcript: input.transcriptBase64,
|
|
694
838
|
messageCount: input.messageCount,
|
|
695
839
|
toolCallCount: input.toolCallCount,
|
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.35",
|
|
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",
|