@supercorks/krnl 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Kernel CLI
2
2
 
3
- Connect [Kernel](https://app.krnl.work) to Codex on your Mac. Start a task from Kernel in Plan or
4
- Work mode, then open the same task in Codex. Conversations and approvals stay in Codex; Kernel
3
+ Connect [Kernel](https://app.krnl.work) to Codex on your Mac. Choose **Launch in Codex** on a Kernel task, review Plan/Work mode, destination, model, reasoning,
4
+ and folder options in the popup, then open the same task in Codex. Conversations and approvals stay in Codex; Kernel
5
5
  shows execution status and a direct link.
6
6
 
7
7
  ## Install and connect
@@ -14,6 +14,8 @@ krnl login
14
14
  krnl connect codex
15
15
  ```
16
16
 
17
+ If pnpm reports `ERR_PNPM_NO_GLOBAL_BIN_DIR`, run `pnpm setup`, open a new Terminal tab, and retry the install.
18
+
17
19
  `krnl login` opens Kernel in your default browser. Sign in with your invited Kernel account and
18
20
  choose **Connect CLI**. Return to Terminal when the browser says the login is finishing.
19
21
 
@@ -24,8 +26,12 @@ instructions if the new hooks need review. Review the eight Kernel hooks and res
24
26
  tasks afterward. The companion detects completed review automatically.
25
27
 
26
28
  Open **Settings → Integrations → Codex** in Kernel to map an organization or project to a saved
27
- Codex project. Your Mac and saved projects appear automatically. Launches stay disabled until setup
28
- is ready; one unavailable project does not disable healthy projects.
29
+ Codex project. Your Mac and saved projects appear automatically. Each connection card contains its organization and
30
+ project mappings. Choose **Add mapping** or **Edit** to set the destination, model, and reasoning.
31
+ New mappings default to **GPT-6 Astra · xhigh**; choices come from that Mac’s current Codex model
32
+ catalog. Launches stay disabled until setup is ready; one unavailable project does not disable
33
+ healthy projects. **Delete connection** revokes its authorization and removes its mappings, while
34
+ keeping existing tasks, handoff history, receipts, and worktrees.
29
35
 
30
36
  No repository checkout, build tools, copied IDs, or per-folder authorization commands are needed.
31
37
  Installing the npm package alone does not start a service or change Codex configuration.
@@ -50,6 +56,10 @@ pnpm add -g @supercorks/krnl@latest
50
56
  krnl connect codex
51
57
  ```
52
58
 
59
+ Version **0.1.1 or later** is required to launch with mapping model and reasoning settings.
60
+ Version **0.1.2 or later** also supports linking an existing Codex task.
61
+ Kernel explains when an older companion needs this update; existing task monitoring continues.
62
+
53
63
  Repeated connection reuses the machine identity. Review changed hook commands in `/hooks` if Node
54
64
  moved. After updating Node, run `krnl connect codex` to refresh the service’s executable path.
55
65
 
@@ -74,6 +84,26 @@ Existing companion installations keep their allowlist until you explicitly upgra
74
84
  `krnl login` and `krnl connect codex`. The upgrade retains their machine identity, mappings, and
75
85
  handoff history when the existing connection is still authorized.
76
86
 
87
+ ## Link an existing task
88
+
89
+ On a Kernel task, choose **Link existing task**. Search by the current Codex task title
90
+ (at least three characters) or paste a `codex://threads/<task-id>` link, select the match,
91
+ and choose **Link task**. Choose a Mac if more than one is connected. Only tasks in
92
+ compatible, locally authorized saved projects appear. Archived tasks are labeled.
93
+
94
+ Linking adds monitoring and a direct Codex link; it never starts a turn, changes its model,
95
+ or changes its folder. A Codex task can be associated with one Kernel task per Mac;
96
+ linking it again to that same task returns the existing association.
97
+
98
+ Search uses a short-lived local metadata cache and never searches messages or previews.
99
+ Kernel receives at most 25 matching titles and destination metadata per lookup. Searches
100
+ expire after 30 seconds, with resolved matches available for two minutes. Refine a broad
101
+ query or paste a deep link if the catalog cannot be searched in time.
102
+
103
+ Completed tasks become Done after their saved turn metadata is checked. A task already
104
+ executing or awaiting input when linked can remain Disconnected until its next status hook;
105
+ continue in Codex normally. Subsequent hooks provide Running, waiting, and Done transitions.
106
+
77
107
  ## Local data and troubleshooting
78
108
 
79
109
  The executable is copied to a stable location under
@@ -91,7 +121,7 @@ revoked the Mac remotely, sign in again with `krnl login` before reconnecting. I
91
121
  reach Kernel, the disconnect command stops the local service and asks you to retry online.
92
122
 
93
123
  Disconnect and logout preserve Codex tasks, local creation receipts, and worktrees. Hooks retain
94
- only lifecycle metadata for tasks created by this connection. Neither transcripts nor attachment
124
+ only lifecycle metadata for tasks launched or explicitly linked through this connection. Neither transcripts nor attachment
95
125
  contents are uploaded. Keep receipts intact when investigating an uncertain launch; the companion
96
126
  will reconcile creation before retrying an acknowledgement and will not repeat an uncertain turn.
97
127
 
@@ -12425,12 +12425,12 @@ function describe(description) {
12425
12425
  return ch;
12426
12426
  }
12427
12427
  // @__NO_SIDE_EFFECTS__
12428
- function meta(metadata) {
12428
+ function meta(metadata2) {
12429
12429
  const ch = new $ZodCheck({ check: "meta" });
12430
12430
  ch._zod.onattach = [
12431
12431
  (inst) => {
12432
12432
  const existing = globalRegistry.get(inst) ?? {};
12433
- globalRegistry.add(inst, { ...existing, ...metadata });
12433
+ globalRegistry.add(inst, { ...existing, ...metadata2 });
12434
12434
  }
12435
12435
  ];
12436
12436
  ch._zod.check = () => {
@@ -15659,15 +15659,15 @@ async function writeJson(path, value) {
15659
15659
  async function processLock(directory) {
15660
15660
  const port = 4e4 + createHash("sha256").update(directory).digest().readUInt16BE(0) % 2e4;
15661
15661
  const server = createServer((socket) => socket.destroy());
15662
- await new Promise((resolve2, reject) => {
15662
+ await new Promise((resolve3, reject) => {
15663
15663
  server.once(
15664
15664
  "error",
15665
15665
  () => reject(new Error("A companion already owns this pairing, or its local lock port is in use"))
15666
15666
  );
15667
- server.listen({ host: "127.0.0.1", port, exclusive: true }, resolve2);
15667
+ server.listen({ host: "127.0.0.1", port, exclusive: true }, resolve3);
15668
15668
  });
15669
15669
  return async () => {
15670
- await new Promise((resolve2) => server.close(() => resolve2()));
15670
+ await new Promise((resolve3) => server.close(() => resolve3()));
15671
15671
  };
15672
15672
  }
15673
15673
  function processAlive(pid) {
@@ -15710,12 +15710,12 @@ async function readCredential(id, keychainService = service) {
15710
15710
  async function saveCredential(id, token, keychainService = service) {
15711
15711
  external_exports.uuid().parse(id);
15712
15712
  external_exports.string().regex(/^[A-Za-z0-9_-]{43,128}$/).parse(token);
15713
- await new Promise((resolve2, reject) => {
15713
+ await new Promise((resolve3, reject) => {
15714
15714
  const child = spawn("/usr/bin/security", ["-i"], { stdio: ["pipe", "ignore", "ignore"] });
15715
15715
  child.on("error", reject);
15716
15716
  child.on(
15717
15717
  "exit",
15718
- (code) => code === 0 ? resolve2() : reject(new Error("Keychain could not save the machine credential"))
15718
+ (code) => code === 0 ? resolve3() : reject(new Error("Keychain could not save the machine credential"))
15719
15719
  );
15720
15720
  child.stdin.end(`add-generic-password -U -s "${keychainService}" -a "${id}" -w "${token}"
15721
15721
  `);
@@ -16016,12 +16016,12 @@ var AppServer = class extends EventEmitter {
16016
16016
  call(method, params, timeoutMs = 3e4) {
16017
16017
  if (this.closed) return Promise.reject(new Error("Codex connection closed"));
16018
16018
  const id = ++this.nextId;
16019
- return new Promise((resolve2, reject) => {
16019
+ return new Promise((resolve3, reject) => {
16020
16020
  const timer = setTimeout(() => {
16021
16021
  this.pending.delete(id);
16022
16022
  reject(new Error(`Codex ${method} acknowledgement is uncertain`));
16023
16023
  }, timeoutMs);
16024
- this.pending.set(id, { resolve: (value) => resolve2(value), reject, timer });
16024
+ this.pending.set(id, { resolve: (value) => resolve3(value), reject, timer });
16025
16025
  this.child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
16026
16026
  });
16027
16027
  }
@@ -16041,11 +16041,11 @@ var AppServer = class extends EventEmitter {
16041
16041
  this.close();
16042
16042
  return;
16043
16043
  }
16044
- await new Promise((resolve2, reject) => {
16044
+ await new Promise((resolve3, reject) => {
16045
16045
  const finish = () => {
16046
16046
  clearTimeout(escalate);
16047
16047
  clearTimeout(deadline);
16048
- resolve2();
16048
+ resolve3();
16049
16049
  };
16050
16050
  const escalate = setTimeout(() => this.child.kill("SIGKILL"), 3e3);
16051
16051
  const deadline = setTimeout(() => {
@@ -22061,6 +22061,25 @@ var reconcileTogglEntrySchema = external_exports.discriminatedUnion("action", [
22061
22061
 
22062
22062
  // ../domain/src/codex.ts
22063
22063
  var CODEX_PROTOCOL_VERSION = 1;
22064
+ var CODEX_DEFAULT_MODEL = "gpt-6-astra";
22065
+ var CODEX_DEFAULT_REASONING_EFFORT = "xhigh";
22066
+ var CODEX_REASONING_EFFORTS = [
22067
+ "none",
22068
+ "minimal",
22069
+ "low",
22070
+ "medium",
22071
+ "high",
22072
+ "xhigh",
22073
+ "max",
22074
+ "ultra"
22075
+ ];
22076
+ var codexReasoningEffortSchema = external_exports.enum(CODEX_REASONING_EFFORTS);
22077
+ var codexModelIdSchema = external_exports.string().trim().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/);
22078
+ var codexModelSchema = external_exports.strictObject({
22079
+ id: codexModelIdSchema,
22080
+ label: external_exports.string().trim().min(1).max(200),
22081
+ reasoningEfforts: external_exports.array(codexReasoningEffortSchema).min(1).max(CODEX_REASONING_EFFORTS.length)
22082
+ });
22064
22083
  var protocolVersion = external_exports.literal(CODEX_PROTOCOL_VERSION);
22065
22084
  var sequence = external_exports.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
22066
22085
  var identity = external_exports.string().min(1).max(200);
@@ -22078,6 +22097,10 @@ var codexCatalogProjectSchema = external_exports.strictObject({
22078
22097
  var codexHealthSchema = external_exports.strictObject({
22079
22098
  reachable: external_exports.boolean(),
22080
22099
  hooksReady: external_exports.boolean(),
22100
+ // Older companions cannot parse model settings in strict launch payloads.
22101
+ supportsModelSelection: external_exports.boolean().optional(),
22102
+ supportsThreadLinking: external_exports.boolean().optional(),
22103
+ models: external_exports.array(codexModelSchema).max(100).optional(),
22081
22104
  appServerVersion: external_exports.string().max(100),
22082
22105
  desktopVersion: external_exports.string().max(100),
22083
22106
  issue: external_exports.string().max(500).nullable()
@@ -22103,6 +22126,8 @@ var saveCodexMappingSchema = external_exports.strictObject({
22103
22126
  machineId: external_exports.uuid(),
22104
22127
  projectId: identity,
22105
22128
  checkout: codexCheckoutSchema,
22129
+ model: codexModelIdSchema.default(CODEX_DEFAULT_MODEL),
22130
+ reasoningEffort: codexReasoningEffortSchema.default(CODEX_DEFAULT_REASONING_EFFORT),
22106
22131
  version: sequence.default(0)
22107
22132
  });
22108
22133
  var deleteCodexMappingSchema = external_exports.strictObject({
@@ -22116,6 +22141,9 @@ var launchCodexTaskSchema = external_exports.strictObject({
22116
22141
  taskId: external_exports.uuid(),
22117
22142
  taskVersion: external_exports.number().int().positive(),
22118
22143
  mode: codexModeSchema,
22144
+ model: codexModelIdSchema.optional(),
22145
+ reasoningEffort: codexReasoningEffortSchema.optional(),
22146
+ checkout: codexCheckoutSchema.optional(),
22119
22147
  override: external_exports.strictObject({ machineId: external_exports.uuid(), projectId: identity }).optional()
22120
22148
  });
22121
22149
  var codexLaunchSchema = external_exports.strictObject({
@@ -22125,6 +22153,8 @@ var codexLaunchSchema = external_exports.strictObject({
22125
22153
  mode: codexModeSchema,
22126
22154
  destination: codexCatalogProjectSchema,
22127
22155
  checkout: codexCheckoutSchema,
22156
+ model: codexModelIdSchema.optional(),
22157
+ reasoningEffort: codexReasoningEffortSchema.optional(),
22128
22158
  submittedAt: external_exports.iso.datetime(),
22129
22159
  expiresAt: external_exports.iso.datetime(),
22130
22160
  task: external_exports.strictObject({
@@ -22196,6 +22226,83 @@ function codexLaunchPrompt(launch) {
22196
22226
  ].join("\n");
22197
22227
  }
22198
22228
 
22229
+ // ../domain/src/codex-threads.ts
22230
+ var CODEX_THREAD_SEARCH_MIN = 3;
22231
+ var CODEX_THREAD_RESULT_LIMIT = 25;
22232
+ var codexTitleQuerySchema = external_exports.string().trim().max(200).refine(
22233
+ (value) => [...value].length >= CODEX_THREAD_SEARCH_MIN,
22234
+ "Enter at least 3 characters to search task titles"
22235
+ );
22236
+ function codexThreadIdFromUrl(value) {
22237
+ try {
22238
+ if (!/^codex:\/\/threads\/[0-9a-f-]+\/?$/i.test(value.trim())) return null;
22239
+ const url2 = new URL(value.trim());
22240
+ if (url2.protocol !== "codex:" || url2.hostname !== "threads" || url2.port || url2.username || url2.password || url2.search || url2.hash)
22241
+ return null;
22242
+ const match = /^\/([^/]+)\/?$/.exec(url2.pathname);
22243
+ const id = external_exports.uuid().safeParse(match?.[1]);
22244
+ return id.success ? id.data.toLowerCase() : null;
22245
+ } catch {
22246
+ return null;
22247
+ }
22248
+ }
22249
+ var base = {
22250
+ protocolVersion: external_exports.literal(1),
22251
+ requestId: external_exports.uuid(),
22252
+ taskId: external_exports.uuid(),
22253
+ machineId: external_exports.uuid()
22254
+ };
22255
+ var startCodexThreadLookupSchema = external_exports.discriminatedUnion("kind", [
22256
+ external_exports.strictObject({ ...base, kind: external_exports.literal("search"), query: codexTitleQuerySchema }),
22257
+ external_exports.strictObject({
22258
+ ...base,
22259
+ kind: external_exports.literal("link"),
22260
+ url: external_exports.string().trim().max(200).refine(
22261
+ (value) => codexThreadIdFromUrl(value) !== null,
22262
+ "Paste a codex://threads/<task-id> link"
22263
+ )
22264
+ })
22265
+ ]);
22266
+ var codexThreadResultSchema = external_exports.strictObject({
22267
+ id: external_exports.uuid(),
22268
+ title: external_exports.string().trim().min(1).max(300),
22269
+ project: codexCatalogProjectSchema,
22270
+ cwd: external_exports.string().min(1).max(4096),
22271
+ updatedAt: external_exports.iso.datetime(),
22272
+ archived: external_exports.boolean()
22273
+ });
22274
+ var codexThreadLookupWorkSchema = external_exports.strictObject({
22275
+ id: external_exports.uuid(),
22276
+ query: codexTitleQuerySchema.nullable(),
22277
+ threadId: external_exports.uuid().nullable(),
22278
+ expiresAt: external_exports.iso.datetime()
22279
+ }).refine(
22280
+ (value) => value.query === null !== (value.threadId === null),
22281
+ "Choose title search or a task link"
22282
+ );
22283
+ var codexThreadLookupResultSchema = external_exports.strictObject({
22284
+ protocolVersion: external_exports.literal(1),
22285
+ connectionId: external_exports.uuid(),
22286
+ generation: external_exports.number().int().nonnegative(),
22287
+ id: external_exports.uuid(),
22288
+ results: external_exports.array(codexThreadResultSchema).max(CODEX_THREAD_RESULT_LIMIT),
22289
+ hasMore: external_exports.boolean(),
22290
+ error: external_exports.string().max(500).nullable()
22291
+ });
22292
+ var linkCodexThreadSchema = external_exports.strictObject({
22293
+ ...base,
22294
+ lookupId: external_exports.uuid(),
22295
+ threadId: external_exports.uuid()
22296
+ });
22297
+ var codexWatchSchema = external_exports.strictObject({
22298
+ id: external_exports.uuid(),
22299
+ machineId: external_exports.uuid(),
22300
+ sequence: external_exports.number().int().nonnegative(),
22301
+ linkedThreadId: external_exports.uuid(),
22302
+ title: external_exports.string().min(1).max(300),
22303
+ destination: codexCatalogProjectSchema
22304
+ });
22305
+
22199
22306
  // ../domain/src/cli.ts
22200
22307
  var CLI_PROTOCOL_VERSION = 1;
22201
22308
  var CLI_LOGIN_TTL_MS = 5 * 6e4;
@@ -22269,6 +22376,257 @@ var settingsHealthSnapshotSchema = external_exports.object({
22269
22376
  })
22270
22377
  });
22271
22378
 
22379
+ // src/models.ts
22380
+ var messages = {
22381
+ catalog: "Codex's model catalog could not be read. Reconnect the companion and try again.",
22382
+ model: "The selected model is unavailable on this Mac. Choose another model in Codex settings.",
22383
+ effort: "The selected model does not support this reasoning effort. Update the Codex settings and try again."
22384
+ };
22385
+ var CodexModelSelectionError = class extends Error {
22386
+ constructor(reason) {
22387
+ super(messages[reason]);
22388
+ }
22389
+ };
22390
+ var modelPageSchema = external_exports.object({
22391
+ data: external_exports.array(
22392
+ external_exports.object({
22393
+ model: codexModelIdSchema,
22394
+ displayName: external_exports.string().trim().min(1),
22395
+ supportedReasoningEfforts: external_exports.array(external_exports.object({ reasoningEffort: external_exports.string() }))
22396
+ })
22397
+ ),
22398
+ nextCursor: external_exports.string().min(1).nullable().optional()
22399
+ });
22400
+ async function discoverModels(server) {
22401
+ try {
22402
+ const models = /* @__PURE__ */ new Map();
22403
+ const cursors = /* @__PURE__ */ new Set();
22404
+ let cursor;
22405
+ do {
22406
+ const page = modelPageSchema.parse(
22407
+ await server.call("model/list", { limit: 100, ...cursor ? { cursor } : {} }, 5e3)
22408
+ );
22409
+ for (const model of page.data) {
22410
+ const reasoningEfforts = model.supportedReasoningEfforts.flatMap((option) => {
22411
+ const parsed = codexReasoningEffortSchema.safeParse(option.reasoningEffort);
22412
+ return parsed.success ? [parsed.data] : [];
22413
+ });
22414
+ if (!reasoningEfforts.length) continue;
22415
+ models.set(model.model, {
22416
+ id: model.model,
22417
+ label: model.displayName.slice(0, 200),
22418
+ reasoningEfforts: [...new Set(reasoningEfforts)]
22419
+ });
22420
+ if (models.size > 100) throw new CodexModelSelectionError("catalog");
22421
+ }
22422
+ cursor = page.nextCursor ?? void 0;
22423
+ if (cursor) {
22424
+ if (cursors.has(cursor) || cursors.size >= 100)
22425
+ throw new CodexModelSelectionError("catalog");
22426
+ cursors.add(cursor);
22427
+ }
22428
+ } while (cursor);
22429
+ return [...models.values()];
22430
+ } catch {
22431
+ throw new CodexModelSelectionError("catalog");
22432
+ }
22433
+ }
22434
+ function resolveModelSelection(launch, models) {
22435
+ const model = launch.model ?? CODEX_DEFAULT_MODEL;
22436
+ const reasoningEffort = launch.reasoningEffort ?? CODEX_DEFAULT_REASONING_EFFORT;
22437
+ const available = models.find((candidate) => candidate.id === model);
22438
+ if (!available) throw new CodexModelSelectionError("model");
22439
+ if (!available.reasoningEfforts.includes(reasoningEffort))
22440
+ throw new CodexModelSelectionError("effort");
22441
+ return { model, reasoningEffort };
22442
+ }
22443
+
22444
+ // src/threads.ts
22445
+ import { realpath as realpath2, stat as stat2 } from "node:fs/promises";
22446
+ import { isAbsolute as isAbsolute2, relative, resolve } from "node:path";
22447
+ var metadata = external_exports.object({
22448
+ id: external_exports.uuid(),
22449
+ name: external_exports.string().nullable().optional(),
22450
+ projectId: external_exports.string().nullable().optional(),
22451
+ cwd: external_exports.string(),
22452
+ updatedAt: external_exports.number().finite().nonnegative()
22453
+ });
22454
+ var pageSchema = external_exports.object({
22455
+ data: external_exports.array(metadata).max(200),
22456
+ nextCursor: external_exports.string().nullable().optional()
22457
+ });
22458
+ var sourceKinds = [
22459
+ "cli",
22460
+ "vscode",
22461
+ "exec",
22462
+ "appServer",
22463
+ "subAgent",
22464
+ "subAgentReview",
22465
+ "subAgentCompact",
22466
+ "subAgentThreadSpawn",
22467
+ "subAgentOther",
22468
+ "unknown"
22469
+ ];
22470
+ var inside = (root, folder) => {
22471
+ const path = relative(root, folder);
22472
+ return !isAbsolute2(path) && path !== ".." && !path.startsWith("../");
22473
+ };
22474
+ async function commonGitDirectory(folder) {
22475
+ try {
22476
+ const { stdout } = await execute("git", ["-C", folder, "rev-parse", "--git-common-dir"], {
22477
+ timeout: 1500,
22478
+ maxBuffer: 8192
22479
+ });
22480
+ return await realpath2(resolve(folder, stdout.trim()));
22481
+ } catch {
22482
+ return null;
22483
+ }
22484
+ }
22485
+ async function resolveThreadProject(thread, pairing, catalog2) {
22486
+ const authorized = catalog2.filter(
22487
+ (project) => !project.issue && (pairing.projectAccess === "all-saved" || pairing.projects.some(
22488
+ (allowed) => allowed.id === project.id && allowed.root === project.root
22489
+ ))
22490
+ );
22491
+ let cwd;
22492
+ try {
22493
+ cwd = await realpath2(thread.cwd);
22494
+ if (!(await stat2(cwd)).isDirectory()) return null;
22495
+ } catch {
22496
+ return null;
22497
+ }
22498
+ const candidates = thread.projectId ? authorized.filter((project) => project.id === thread.projectId) : [...authorized].sort((a2, b2) => b2.root.length - a2.root.length);
22499
+ for (const project of candidates) {
22500
+ if (await realpath2(project.root).catch(() => null) !== project.root) continue;
22501
+ if (inside(project.root, cwd)) return { project, cwd };
22502
+ }
22503
+ const common = candidates.some((project) => project.isGit) ? await commonGitDirectory(cwd) : null;
22504
+ if (common)
22505
+ for (const project of candidates) {
22506
+ if (project.isGit && await commonGitDirectory(project.root) === common)
22507
+ return { project, cwd };
22508
+ }
22509
+ return null;
22510
+ }
22511
+ var CodexThreadLookupService = class {
22512
+ constructor(now = () => performance.now()) {
22513
+ this.now = now;
22514
+ }
22515
+ now;
22516
+ cached = null;
22517
+ refreshedAt = 0;
22518
+ loading;
22519
+ index(server) {
22520
+ if (this.cached && this.now() - this.refreshedAt < 3e4) return Promise.resolve(this.cached);
22521
+ if (this.loading) return this.loading;
22522
+ this.loading = (async () => {
22523
+ const threads = [];
22524
+ const started = this.now();
22525
+ for (const archived of [false, true]) {
22526
+ let cursor = null;
22527
+ const visited = /* @__PURE__ */ new Set();
22528
+ do {
22529
+ if (this.now() - started > 2e4 || threads.length > 5e4)
22530
+ throw new Error(
22531
+ "The task catalog is too large to search right now. Paste a Codex task link instead."
22532
+ );
22533
+ const page = pageSchema.parse(
22534
+ await server.call(
22535
+ "thread/list",
22536
+ {
22537
+ limit: 200,
22538
+ archived,
22539
+ modelProviders: [],
22540
+ sourceKinds,
22541
+ useStateDbOnly: true,
22542
+ sortKey: "updated_at",
22543
+ ...cursor ? { cursor } : {}
22544
+ },
22545
+ 5e3
22546
+ )
22547
+ );
22548
+ threads.push(...page.data.map((thread) => ({ ...thread, archived })));
22549
+ cursor = page.nextCursor ?? null;
22550
+ if (cursor && visited.has(cursor))
22551
+ throw new Error("Codex returned an invalid task catalog. Try again.");
22552
+ if (cursor) visited.add(cursor);
22553
+ } while (cursor);
22554
+ }
22555
+ this.cached = [...new Map(threads.map((thread) => [thread.id, thread])).values()].sort(
22556
+ (a2, b2) => b2.updatedAt - a2.updatedAt || a2.id.localeCompare(b2.id)
22557
+ );
22558
+ this.refreshedAt = this.now();
22559
+ return this.cached;
22560
+ })().finally(() => {
22561
+ this.loading = void 0;
22562
+ });
22563
+ return this.loading;
22564
+ }
22565
+ async lookup(server, raw, pairing, catalog2) {
22566
+ const work = codexThreadLookupWorkSchema.parse(raw);
22567
+ let candidates;
22568
+ if (work.threadId) {
22569
+ const result = external_exports.object({ thread: metadata }).parse(
22570
+ await server.call("thread/read", { threadId: work.threadId, includeTurns: false }, 5e3)
22571
+ );
22572
+ if (result.thread.id !== work.threadId) throw new Error("Codex returned a different task.");
22573
+ candidates = [{ ...result.thread, archived: false }];
22574
+ } else {
22575
+ const query = work.query.toLowerCase();
22576
+ candidates = (await this.index(server)).filter(
22577
+ (thread) => thread.name?.trim().slice(0, 300).toLowerCase().includes(query)
22578
+ );
22579
+ }
22580
+ const results = [];
22581
+ const started = this.now();
22582
+ for (const thread of candidates) {
22583
+ if (this.now() - started > 2e4)
22584
+ throw new Error("Refine the title or paste a Codex task link.");
22585
+ const destination = await resolveThreadProject(thread, pairing, catalog2);
22586
+ if (!destination) continue;
22587
+ if (results.length === CODEX_THREAD_RESULT_LIMIT) return { results, hasMore: true };
22588
+ results.push({
22589
+ id: thread.id,
22590
+ title: thread.name?.trim().slice(0, 300) || "Untitled task",
22591
+ ...destination,
22592
+ updatedAt: new Date(thread.updatedAt * 1e3).toISOString(),
22593
+ archived: thread.archived
22594
+ });
22595
+ }
22596
+ return { results, hasMore: false };
22597
+ }
22598
+ };
22599
+ async function linkedReceipt(server, watch, pairing, catalog2) {
22600
+ if (watch.machineId !== pairing.machineId)
22601
+ throw new Error("This task belongs to another machine");
22602
+ const { thread } = external_exports.object({ thread: metadata }).parse(
22603
+ await server.call(
22604
+ "thread/read",
22605
+ { threadId: watch.linkedThreadId, includeTurns: false },
22606
+ 5e3
22607
+ )
22608
+ );
22609
+ if (thread.id !== watch.linkedThreadId) throw new Error("Codex returned a different task");
22610
+ const destination = await resolveThreadProject(thread, pairing, catalog2);
22611
+ if (!destination || destination.project.id !== watch.destination.id || destination.project.root !== watch.destination.root)
22612
+ throw new Error("The saved project is no longer locally authorized");
22613
+ return {
22614
+ launch: watch,
22615
+ preparedAt: Date.now(),
22616
+ phase: "released",
22617
+ threadId: thread.id,
22618
+ cwd: destination.cwd,
22619
+ status: {
22620
+ launchId: watch.id,
22621
+ sequence: watch.sequence + 1,
22622
+ state: "unreachable",
22623
+ waiting: false,
22624
+ turnId: null,
22625
+ message: "Checking the linked task in Codex. Activity is observed from the next status hook."
22626
+ }
22627
+ };
22628
+ }
22629
+
22272
22630
  // src/runtime.ts
22273
22631
  var sourceMarker = (id) => `kernel-handoff:${id}`;
22274
22632
  function initialReceipt(launch) {
@@ -22287,7 +22645,7 @@ function initialReceipt(launch) {
22287
22645
  };
22288
22646
  }
22289
22647
  function canBegin(receipt, now) {
22290
- return receipt.phase === "prepared" && now < Date.parse(receipt.launch.expiresAt);
22648
+ return receipt.phase === "prepared" && "expiresAt" in receipt.launch && now < Date.parse(receipt.launch.expiresAt);
22291
22649
  }
22292
22650
  async function findCreation(server, receipt) {
22293
22651
  let cursor = null;
@@ -22316,6 +22674,8 @@ async function runCompanion(machineId, entrypoint2, signal) {
22316
22674
  const unlock = await processLock(directory);
22317
22675
  const owners = /* @__PURE__ */ new Map();
22318
22676
  let observer;
22677
+ let lookupServer;
22678
+ let lookupWork;
22319
22679
  try {
22320
22680
  const pairing = await readJson(join4(directory, "pairing.json"));
22321
22681
  if (!pairing?.workspaceId) throw new Error("Complete this machine pairing first");
@@ -22343,6 +22703,26 @@ async function runCompanion(machineId, entrypoint2, signal) {
22343
22703
  )
22344
22704
  );
22345
22705
  const queue = [];
22706
+ const lookupService = new CodexThreadLookupService();
22707
+ let lookupBusy = false;
22708
+ const lookupResults = [];
22709
+ const watched = /* @__PURE__ */ new Map();
22710
+ const watchAttempts = /* @__PURE__ */ new Map();
22711
+ const watchResults = [];
22712
+ let watchCursor = null;
22713
+ const connectLookup = async () => {
22714
+ if (!lookupServer) {
22715
+ lookupServer = new AppServer(directory);
22716
+ try {
22717
+ await lookupServer.initialize();
22718
+ } catch (error51) {
22719
+ lookupServer.close();
22720
+ lookupServer = void 0;
22721
+ throw error51;
22722
+ }
22723
+ }
22724
+ return lookupServer;
22725
+ };
22346
22726
  const connectObserver = async () => {
22347
22727
  observer?.close();
22348
22728
  observer = new AppServer(directory);
@@ -22353,6 +22733,9 @@ async function runCompanion(machineId, entrypoint2, signal) {
22353
22733
  let health = {
22354
22734
  reachable: false,
22355
22735
  hooksReady: false,
22736
+ supportsModelSelection: true,
22737
+ supportsThreadLinking: true,
22738
+ models: [],
22356
22739
  appServerVersion: "",
22357
22740
  desktopVersion: "",
22358
22741
  issue: "Checking Codex compatibility."
@@ -22418,16 +22801,22 @@ async function runCompanion(machineId, entrypoint2, signal) {
22418
22801
  () => queue.push({ launchId: launch.id, event: { method: "kernel/closed" } })
22419
22802
  );
22420
22803
  await owner.initialize();
22804
+ const selection = resolveModelSelection(launch, await discoverModels(owner));
22805
+ if (!canBegin(receipt, Date.now() + clockOffset))
22806
+ throw new Error("The launch expired while checking model availability");
22807
+ receipt.model = selection.model;
22808
+ receipt.effort = selection.reasoningEffort;
22421
22809
  receipt.phase = "creating";
22422
22810
  await save(receipt);
22423
22811
  const created = await owner.call("thread/start", {
22424
22812
  cwd: receipt.cwd,
22425
22813
  projectId: project.id,
22426
- threadSource: sourceMarker(launch.id)
22814
+ threadSource: sourceMarker(launch.id),
22815
+ model: selection.model,
22816
+ config: { model_reasoning_effort: selection.reasoningEffort },
22817
+ allowProviderModelFallback: false
22427
22818
  });
22428
22819
  receipt.threadId = created.thread.id;
22429
- receipt.model = created.model;
22430
- receipt.effort = created.reasoningEffort ?? null;
22431
22820
  receipt.phase = "created";
22432
22821
  await save(receipt);
22433
22822
  await indexThreads();
@@ -22442,14 +22831,14 @@ async function runCompanion(machineId, entrypoint2, signal) {
22442
22831
  input: [{ type: "text", text: codexLaunchPrompt(launch) }],
22443
22832
  collaborationMode: collaborationMode(
22444
22833
  launch.mode,
22445
- created.model,
22446
- created.reasoningEffort ?? null
22834
+ selection.model,
22835
+ selection.reasoningEffort
22447
22836
  )
22448
22837
  });
22449
22838
  receipt.turnId = response.turn.id;
22450
22839
  receipt.status.turnId = response.turn.id;
22451
22840
  await save(receipt);
22452
- } catch {
22841
+ } catch (error51) {
22453
22842
  try {
22454
22843
  await owners.get(launch.id)?.stop();
22455
22844
  } catch {
@@ -22474,7 +22863,7 @@ async function runCompanion(machineId, entrypoint2, signal) {
22474
22863
  };
22475
22864
  } else {
22476
22865
  receipt.ambiguous = receipt.phase === "creating";
22477
- receipt.failure = receipt.ambiguous ? "Confirming whether Codex created the task. This launch will not be repeated." : "The task could not start. Check the locally authorized project and checkout policy.";
22866
+ receipt.failure = receipt.ambiguous ? "Confirming whether Codex created the task. This launch will not be repeated." : error51 instanceof CodexModelSelectionError ? error51.message : "The task could not start. Check the locally authorized project and checkout policy.";
22478
22867
  if (!receipt.ambiguous) receipt.phase = "failed";
22479
22868
  }
22480
22869
  await save(receipt);
@@ -22518,21 +22907,26 @@ async function runCompanion(machineId, entrypoint2, signal) {
22518
22907
  "Print:CFBundleShortVersionString",
22519
22908
  "/Applications/ChatGPT.app/Contents/Info.plist"
22520
22909
  ]).then((result) => result.stdout.trim()).catch(() => "");
22910
+ const models = await discoverModels(observer);
22521
22911
  health = {
22522
22912
  reachable: true,
22523
22913
  hooksReady: ready,
22914
+ supportsModelSelection: true,
22915
+ supportsThreadLinking: true,
22916
+ models,
22524
22917
  appServerVersion: stdout.trim(),
22525
22918
  desktopVersion: desktop,
22526
22919
  issue: ready ? null : "Review the eight Kernel hooks using /hooks in Codex CLI. The companion detects completed review automatically."
22527
22920
  };
22528
- } catch {
22921
+ } catch (error51) {
22529
22922
  observer?.close();
22530
22923
  observer = void 0;
22531
22924
  health = {
22532
22925
  ...health,
22533
22926
  reachable: false,
22534
22927
  hooksReady: false,
22535
- issue: "Codex could not be reached through its supported app-server interface."
22928
+ models: [],
22929
+ issue: error51 instanceof CodexModelSelectionError ? error51.message : "Codex could not be reached through its supported app-server interface."
22536
22930
  };
22537
22931
  }
22538
22932
  try {
@@ -22596,6 +22990,18 @@ async function runCompanion(machineId, entrypoint2, signal) {
22596
22990
  if (!reconciled || recovered) await indexThreads();
22597
22991
  reconciled = true;
22598
22992
  }
22993
+ for (const receipt of watchResults.splice(0)) {
22994
+ if (receipts.has(receipt.launch.id)) continue;
22995
+ receipts.set(receipt.launch.id, receipt);
22996
+ await save(receipt);
22997
+ await indexThreads();
22998
+ }
22999
+ if (!networkError) {
23000
+ while (lookupResults.length) {
23001
+ await post("thread-lookup-result", lookupResults[0]);
23002
+ lookupResults.shift();
23003
+ }
23004
+ }
22599
23005
  for (const { launchId, event } of queue.splice(0)) {
22600
23006
  const receipt = receipts.get(launchId);
22601
23007
  if (!receipt || receipt.phase === "released" || receipt.phase === "failed") continue;
@@ -22710,6 +23116,49 @@ async function runCompanion(machineId, entrypoint2, signal) {
22710
23116
  }
22711
23117
  if (networkError) throw networkError;
22712
23118
  if (health.reachable && health.hooksReady && reconciled && Date.now() - tickStarted <= 5e3) {
23119
+ const lookups = await post("thread-lookups", { cursor: watchCursor });
23120
+ watchCursor = lookups.nextCursor;
23121
+ for (const raw of lookups.watches) {
23122
+ const watch = codexWatchSchema.parse(raw);
23123
+ if (watch.machineId === machineId && !receipts.has(watch.id))
23124
+ watched.set(watch.id, watch);
23125
+ }
23126
+ if (!lookupBusy) {
23127
+ const request = lookups.requests.find(
23128
+ (item) => Date.parse(item.expiresAt) > Date.now() + clockOffset
23129
+ );
23130
+ const watch = [...watched.values()].find(
23131
+ (item) => Date.now() - (watchAttempts.get(item.id) ?? 0) > 3e4
23132
+ );
23133
+ if (request || watch) {
23134
+ lookupBusy = true;
23135
+ if (!request && watch) watchAttempts.set(watch.id, Date.now());
23136
+ lookupWork = (async () => {
23137
+ const local = await readJson(join4(directory, "pairing.json"));
23138
+ if (!local) throw new Error("The local connection is unavailable");
23139
+ const server = await connectLookup();
23140
+ if (request) {
23141
+ const result = await lookupService.lookup(server, request, local, catalog2);
23142
+ lookupResults.push({ id: request.id, ...result, error: null });
23143
+ } else if (watch) {
23144
+ watchResults.push(await linkedReceipt(server, watch, local, catalog2));
23145
+ watched.delete(watch.id);
23146
+ }
23147
+ })().catch(() => {
23148
+ lookupServer?.close();
23149
+ lookupServer = void 0;
23150
+ if (request)
23151
+ lookupResults.push({
23152
+ id: request.id,
23153
+ results: [],
23154
+ hasMore: false,
23155
+ error: "The local task could not be read. Check Codex and the saved project, then try again."
23156
+ });
23157
+ }).finally(() => {
23158
+ lookupBusy = false;
23159
+ });
23160
+ }
23161
+ }
22713
23162
  const response = await post("launches", {});
22714
23163
  for (const launch of response.launches) await begin(codexLaunchSchema.parse(launch));
22715
23164
  }
@@ -22726,6 +23175,8 @@ async function runCompanion(machineId, entrypoint2, signal) {
22726
23175
  } finally {
22727
23176
  await Promise.allSettled([...owners.values()].map((server) => server.stop()));
22728
23177
  await observer?.stop().catch(() => void 0);
23178
+ await lookupServer?.stop().catch(() => void 0);
23179
+ await lookupWork;
22729
23180
  await unlock();
22730
23181
  }
22731
23182
  }
@@ -22783,10 +23234,10 @@ async function signedInProfile() {
22783
23234
  }
22784
23235
  async function loginCallback(state, timeoutMs = 3e5) {
22785
23236
  cliSecretSchema.parse(state);
22786
- let resolve2;
23237
+ let resolve3;
22787
23238
  let reject;
22788
23239
  const result = new Promise((yes, no2) => {
22789
- resolve2 = yes;
23240
+ resolve3 = yes;
22790
23241
  reject = no2;
22791
23242
  });
22792
23243
  void result.catch(() => void 0);
@@ -22823,11 +23274,11 @@ async function loginCallback(state, timeoutMs = 3e5) {
22823
23274
  `<html><title>Kernel CLI</title><body style="font:18px system-ui;padding:48px"><h1>${cancelled ? "Login cancelled" : "Return to your terminal"}</h1><p>${cancelled ? "No CLI login was granted." : "Kernel is finishing your CLI login. You can close this tab."}</p></body></html>`
22824
23275
  );
22825
23276
  if (cancelled) reject(new Error("Login cancelled. No connection was created."));
22826
- else resolve2(code);
23277
+ else resolve3(code);
22827
23278
  });
22828
- await new Promise((resolve3, reject2) => {
23279
+ await new Promise((resolve4, reject2) => {
22829
23280
  server.once("error", reject2);
22830
- server.listen(0, "127.0.0.1", resolve3);
23281
+ server.listen(0, "127.0.0.1", resolve4);
22831
23282
  });
22832
23283
  port = server.address().port;
22833
23284
  const timer = setTimeout(
@@ -22837,7 +23288,7 @@ async function loginCallback(state, timeoutMs = 3e5) {
22837
23288
  const close = async () => {
22838
23289
  clearTimeout(timer);
22839
23290
  server.closeAllConnections();
22840
- await new Promise((resolve3) => server.close(() => resolve3()));
23291
+ await new Promise((resolve4) => server.close(() => resolve4()));
22841
23292
  };
22842
23293
  return { redirectUri: `http://127.0.0.1:${port}/callback`, result, close };
22843
23294
  }
@@ -22918,12 +23369,12 @@ import { createInterface } from "node:readline/promises";
22918
23369
  // src/setup.ts
22919
23370
  import { chmod as chmod2, copyFile, mkdir as mkdir4, rename as rename2, rm } from "node:fs/promises";
22920
23371
  import { homedir as homedir4 } from "node:os";
22921
- import { dirname as dirname2, join as join6, resolve } from "node:path";
23372
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "node:path";
22922
23373
  import { randomUUID as randomUUID5 } from "node:crypto";
22923
23374
  var installedEntrypoint = join6(dataRoot, "bin/kernel-companion.mjs");
22924
23375
  async function installBundle(entrypoint2) {
22925
23376
  await mkdir4(dirname2(installedEntrypoint), { recursive: true, mode: 448 });
22926
- if (resolve(entrypoint2) !== resolve(installedEntrypoint)) {
23377
+ if (resolve2(entrypoint2) !== resolve2(installedEntrypoint)) {
22927
23378
  const temporary = `${installedEntrypoint}.${randomUUID5()}.tmp`;
22928
23379
  try {
22929
23380
  await copyFile(entrypoint2, temporary);
@@ -23243,7 +23694,7 @@ async function main() {
23243
23694
  }
23244
23695
  if (command === "--version") {
23245
23696
  process.stdout.write(
23246
- `${false ? "development" : "0.1.0"}
23697
+ `${false ? "development" : "0.1.2"}
23247
23698
  `
23248
23699
  );
23249
23700
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/krnl",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Connect Kernel to Codex on your Mac",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,4 +17,4 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  }
20
- }
20
+ }