selftune 0.2.8 → 0.2.10

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.
Files changed (140) hide show
  1. package/README.md +35 -35
  2. package/apps/local-dashboard/dist/assets/index-BZVLv70T.js +16 -0
  3. package/apps/local-dashboard/dist/assets/{index-CRtLkBTi.css → index-Bs3Y4ixf.css} +1 -1
  4. package/apps/local-dashboard/dist/assets/{vendor-react-BQH_6WrG.js → vendor-react-BXP54cYo.js} +4 -4
  5. package/apps/local-dashboard/dist/assets/{vendor-table-dK1QMLq9.js → vendor-table-DTF_SXoy.js} +1 -1
  6. package/apps/local-dashboard/dist/assets/{vendor-ui-CO2mrx6e.js → vendor-ui-CWU0d1wd.js} +66 -66
  7. package/apps/local-dashboard/dist/index.html +15 -15
  8. package/bin/selftune.cjs +1 -1
  9. package/cli/selftune/activation-rules.ts +37 -18
  10. package/cli/selftune/agent-guidance.ts +16 -16
  11. package/cli/selftune/alpha-identity.ts +1 -2
  12. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  13. package/cli/selftune/alpha-upload/flush.ts +2 -2
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +106 -3
  15. package/cli/selftune/auth/device-code.ts +32 -0
  16. package/cli/selftune/auto-update.ts +12 -0
  17. package/cli/selftune/badge/badge.ts +1 -0
  18. package/cli/selftune/canonical-export.ts +5 -0
  19. package/cli/selftune/claude-agents.ts +154 -0
  20. package/cli/selftune/contribute/bundle.ts +2 -0
  21. package/cli/selftune/contribute/contribute.ts +1 -0
  22. package/cli/selftune/cron/setup.ts +2 -2
  23. package/cli/selftune/dashboard-contract.ts +1 -1
  24. package/cli/selftune/dashboard-server.ts +11 -52
  25. package/cli/selftune/eval/hooks-to-evals.ts +13 -6
  26. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  27. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  28. package/cli/selftune/eval/unit-test.ts +1 -0
  29. package/cli/selftune/evolution/deploy-proposal.ts +1 -0
  30. package/cli/selftune/evolution/evolve-body.ts +93 -6
  31. package/cli/selftune/evolution/evolve.ts +0 -1
  32. package/cli/selftune/evolution/propose-body.ts +3 -2
  33. package/cli/selftune/evolution/propose-routing.ts +3 -2
  34. package/cli/selftune/evolution/refine-body.ts +3 -2
  35. package/cli/selftune/export.ts +1 -0
  36. package/cli/selftune/grading/auto-grade.ts +1 -0
  37. package/cli/selftune/grading/grade-session.ts +9 -0
  38. package/cli/selftune/hooks/auto-activate.ts +6 -0
  39. package/cli/selftune/hooks/evolution-guard.ts +12 -15
  40. package/cli/selftune/hooks/prompt-log.ts +1 -0
  41. package/cli/selftune/hooks/session-stop.ts +34 -40
  42. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  43. package/cli/selftune/hooks/skill-eval.ts +1 -1
  44. package/cli/selftune/index.ts +23 -14
  45. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  46. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  47. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  48. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  49. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  50. package/cli/selftune/init.ts +197 -96
  51. package/cli/selftune/localdb/db.ts +1 -0
  52. package/cli/selftune/localdb/direct-write.ts +93 -12
  53. package/cli/selftune/localdb/materialize.ts +2 -0
  54. package/cli/selftune/localdb/queries.ts +210 -0
  55. package/cli/selftune/localdb/schema.ts +72 -1
  56. package/cli/selftune/monitoring/watch.ts +1 -0
  57. package/cli/selftune/normalization.ts +4 -0
  58. package/cli/selftune/observability.ts +14 -7
  59. package/cli/selftune/orchestrate.ts +15 -37
  60. package/cli/selftune/repair/skill-usage.ts +7 -3
  61. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  62. package/cli/selftune/routes/overview.ts +1 -0
  63. package/cli/selftune/routes/skill-report.ts +1 -0
  64. package/cli/selftune/sync.ts +31 -1
  65. package/cli/selftune/types.ts +2 -2
  66. package/cli/selftune/uninstall.ts +412 -0
  67. package/cli/selftune/utils/canonical-log.ts +2 -0
  68. package/cli/selftune/utils/jsonl.ts +1 -0
  69. package/cli/selftune/utils/llm-call.ts +131 -3
  70. package/cli/selftune/utils/skill-log.ts +1 -0
  71. package/cli/selftune/utils/transcript.ts +1 -0
  72. package/cli/selftune/utils/trigger-check.ts +1 -1
  73. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  74. package/cli/selftune/workflows/workflows.ts +1 -0
  75. package/package.json +38 -33
  76. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  77. package/packages/telemetry-contract/package.json +3 -3
  78. package/packages/telemetry-contract/src/index.ts +0 -1
  79. package/packages/telemetry-contract/src/schemas.ts +6 -24
  80. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  81. package/packages/ui/README.md +35 -34
  82. package/packages/ui/package.json +3 -3
  83. package/packages/ui/src/components/ActivityTimeline.tsx +49 -42
  84. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  85. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  86. package/packages/ui/src/components/InfoTip.tsx +4 -3
  87. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  88. package/packages/ui/src/components/section-cards.tsx +19 -24
  89. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  90. package/packages/ui/src/lib/constants.tsx +1 -0
  91. package/packages/ui/src/primitives/badge.tsx +12 -15
  92. package/packages/ui/src/primitives/button.tsx +7 -7
  93. package/packages/ui/src/primitives/card.tsx +15 -26
  94. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  95. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  96. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  97. package/packages/ui/src/primitives/label.tsx +6 -6
  98. package/packages/ui/src/primitives/select.tsx +28 -37
  99. package/packages/ui/src/primitives/table.tsx +17 -44
  100. package/packages/ui/src/primitives/tabs.tsx +14 -21
  101. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  102. package/skill/SKILL.md +72 -59
  103. package/skill/Workflows/AlphaUpload.md +4 -4
  104. package/skill/Workflows/AutoActivation.md +11 -6
  105. package/skill/Workflows/Badge.md +22 -16
  106. package/skill/Workflows/Baseline.md +34 -36
  107. package/skill/Workflows/Composability.md +16 -11
  108. package/skill/Workflows/Contribute.md +26 -21
  109. package/skill/Workflows/Cron.md +23 -22
  110. package/skill/Workflows/Dashboard.md +40 -40
  111. package/skill/Workflows/Doctor.md +40 -34
  112. package/skill/Workflows/Evals.md +48 -47
  113. package/skill/Workflows/EvolutionMemory.md +31 -21
  114. package/skill/Workflows/Evolve.md +84 -82
  115. package/skill/Workflows/EvolveBody.md +58 -47
  116. package/skill/Workflows/Grade.md +16 -13
  117. package/skill/Workflows/ImportSkillsBench.md +9 -6
  118. package/skill/Workflows/Ingest.md +36 -21
  119. package/skill/Workflows/Initialize.md +138 -97
  120. package/skill/Workflows/Orchestrate.md +22 -16
  121. package/skill/Workflows/Replay.md +12 -7
  122. package/skill/Workflows/Rollback.md +13 -6
  123. package/skill/Workflows/Schedule.md +6 -6
  124. package/skill/Workflows/Sync.md +18 -11
  125. package/skill/Workflows/UnitTest.md +28 -17
  126. package/skill/Workflows/Watch.md +28 -21
  127. package/skill/agents/diagnosis-analyst.md +11 -0
  128. package/skill/agents/evolution-reviewer.md +15 -1
  129. package/skill/agents/integration-guide.md +10 -0
  130. package/skill/agents/pattern-analyst.md +12 -1
  131. package/skill/references/grading-methodology.md +23 -24
  132. package/skill/references/interactive-config.md +7 -7
  133. package/skill/references/invocation-taxonomy.md +22 -20
  134. package/skill/references/logs.md +20 -6
  135. package/skill/references/setup-patterns.md +4 -2
  136. package/.claude/agents/diagnosis-analyst.md +0 -156
  137. package/.claude/agents/evolution-reviewer.md +0 -180
  138. package/.claude/agents/integration-guide.md +0 -212
  139. package/.claude/agents/pattern-analyst.md +0 -160
  140. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +0 -15
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
+
8
9
  import { getDb } from "./localdb/db.js";
9
10
  import {
10
11
  getOrchestrateRuns,
@@ -101,6 +101,7 @@ Options:
101
101
  telRecords = querySessionTelemetry(db) as SessionTelemetryRecord[];
102
102
  skillUsageRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
103
103
  } else {
104
+ // Intentional JSONL fallback: custom --telemetry-log path overrides SQLite reads
104
105
  telRecords = readJsonl<SessionTelemetryRecord>(telemetryLog);
105
106
  skillUsageRecords = [];
106
107
  }
@@ -812,6 +812,7 @@ Options:
812
812
  telRecords = querySessionTelemetry(db) as SessionTelemetryRecord[];
813
813
  skillUsageRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
814
814
  } else {
815
+ // Intentional JSONL fallback: custom --telemetry-log path overrides SQLite reads
815
816
  telRecords = readJsonl<SessionTelemetryRecord>(telemetryLog);
816
817
  skillUsageRecords = [];
817
818
  }
@@ -883,6 +884,14 @@ Options:
883
884
  }
884
885
  writeFileSync(outputPath, JSON.stringify(result, null, 2), "utf-8");
885
886
 
887
+ // Persist to SQLite for upload staging (fail-open)
888
+ try {
889
+ const { writeGradingResultToDb } = await import("../localdb/direct-write.js");
890
+ writeGradingResultToDb(result);
891
+ } catch {
892
+ // fail-open: grading file is already written above
893
+ }
894
+
886
895
  printSummary(result);
887
896
  console.log(`\nWrote ${outputPath}`);
888
897
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { dirname } from "node:path";
14
+
14
15
  import {
15
16
  CLAUDE_SETTINGS_PATH,
16
17
  EVOLUTION_AUDIT_LOG,
@@ -158,6 +159,11 @@ if (import.meta.main) {
158
159
  // Dynamically import default rules (keeps hook file lightweight)
159
160
  const { DEFAULT_RULES } = await import("../activation-rules.js");
160
161
 
162
+ /**
163
+ * The *_log_path fields exist for test overrides only; default code paths
164
+ * in activation-rules.ts read from SQLite when the path matches the
165
+ * constant (QUERY_LOG, EVOLUTION_AUDIT_LOG, etc.).
166
+ */
161
167
  const ctx: ActivationContext = {
162
168
  session_id: sessionId,
163
169
  query_log_path: QUERY_LOG,
@@ -16,8 +16,8 @@
16
16
 
17
17
  import { existsSync, readFileSync } from "node:fs";
18
18
  import { basename, dirname, join } from "node:path";
19
- import { EVOLUTION_AUDIT_LOG, SELFTUNE_CONFIG_DIR } from "../constants.js";
20
19
 
20
+ import { EVOLUTION_AUDIT_LOG, SELFTUNE_CONFIG_DIR } from "../constants.js";
21
21
  import type { PreToolUsePayload } from "../types.js";
22
22
  import { readJsonl } from "../utils/jsonl.js";
23
23
 
@@ -35,12 +35,12 @@ function extractSkillName(filePath: string): string {
35
35
  }
36
36
 
37
37
  // ---------------------------------------------------------------------------
38
- // Active monitoring check (reads audit log directly no evolution imports)
38
+ // Active monitoring check (SQLite-first JSONL only for test/custom paths)
39
39
  // ---------------------------------------------------------------------------
40
40
 
41
41
  /**
42
42
  * Check if a skill has an active deployed evolution (meaning it's under monitoring).
43
- * Reads the evolution audit JSONL directly to respect architecture lint rules.
43
+ * SQLite is the default read path; JSONL is used only for test/custom-path overrides.
44
44
  *
45
45
  * A skill is "actively monitored" if its last audit action is "deployed".
46
46
  * If the last action is "rolled_back", it's no longer monitored.
@@ -49,21 +49,18 @@ export async function checkActiveMonitoring(
49
49
  skillName: string,
50
50
  auditLogPath: string,
51
51
  ): Promise<boolean> {
52
- // Try SQLite first, fall back to JSONL for non-default paths (e.g., tests)
52
+ // SQLite is the default path; JSONL fallback only for non-default paths (tests)
53
53
  let entries: Array<{ skill_name?: string; action: string }>;
54
54
  if (auditLogPath === EVOLUTION_AUDIT_LOG) {
55
- try {
56
- const { getDb } = await import("../localdb/db.js");
57
- const { queryEvolutionAudit } = await import("../localdb/queries.js");
58
- const db = getDb();
59
- entries = queryEvolutionAudit(db, skillName) as Array<{
60
- skill_name?: string;
61
- action: string;
62
- }>;
63
- } catch {
64
- entries = readJsonl<{ skill_name?: string; action: string }>(auditLogPath);
65
- }
55
+ const { getDb } = await import("../localdb/db.js");
56
+ const { queryEvolutionAudit } = await import("../localdb/queries.js");
57
+ const db = getDb();
58
+ entries = queryEvolutionAudit(db, skillName) as Array<{
59
+ skill_name?: string;
60
+ action: string;
61
+ }>;
66
62
  } else {
63
+ // test/custom-path fallback
67
64
  entries = readJsonl<{ skill_name?: string; action: string }>(auditLogPath);
68
65
  }
69
66
 
@@ -11,6 +11,7 @@
11
11
  import { readdirSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
+
14
15
  import { CANONICAL_LOG, QUERY_LOG, SKIP_PREFIXES } from "../constants.js";
15
16
  import {
16
17
  appendCanonicalRecord,
@@ -9,9 +9,9 @@
9
9
  */
10
10
 
11
11
  import { execSync } from "node:child_process";
12
- import { closeSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
13
- import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
12
+ import { readFileSync } from "node:fs";
14
13
 
14
+ import { CANONICAL_LOG, ORCHESTRATE_LOCK, TELEMETRY_LOG } from "../constants.js";
15
15
  import {
16
16
  appendCanonicalRecords,
17
17
  buildCanonicalExecutionFact,
@@ -25,6 +25,22 @@ import { parseTranscript } from "../utils/transcript.js";
25
25
 
26
26
  const LOCK_STALE_MS = 30 * 60 * 1000;
27
27
 
28
+ interface ReactiveSpawnDeps {
29
+ spawnOrchestrate?: () => boolean;
30
+ }
31
+
32
+ function hasFreshOrchestrateLock(lockPath: string): boolean {
33
+ try {
34
+ const lockContent = readFileSync(lockPath, "utf8");
35
+ const lock = JSON.parse(lockContent) as { timestamp?: string };
36
+ if (typeof lock.timestamp !== "string") return false;
37
+ const lockAge = Date.now() - new Date(lock.timestamp).getTime();
38
+ return Number.isFinite(lockAge) && lockAge < LOCK_STALE_MS;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  /**
29
45
  * Check for pending improvement signals and spawn a focused orchestrate run
30
46
  * in the background if warranted. Fire-and-forget — the hook exits immediately.
@@ -33,6 +49,7 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
33
49
  */
34
50
  export async function maybeSpawnReactiveOrchestrate(
35
51
  lockPath: string = ORCHESTRATE_LOCK,
52
+ deps: ReactiveSpawnDeps = {},
36
53
  ): Promise<boolean> {
37
54
  try {
38
55
  // Read pending signals from SQLite (dynamic import to reduce hook startup cost)
@@ -42,48 +59,25 @@ export async function maybeSpawnReactiveOrchestrate(
42
59
  const pending = queryImprovementSignals(db, false);
43
60
  if (pending.length === 0) return false;
44
61
 
45
- // Atomically claim the lock openSync with "wx" fails if file exists
46
- let fd: number;
47
- try {
48
- fd = openSync(lockPath, "wx");
49
- writeFileSync(fd, JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }));
50
- closeSync(fd);
51
- } catch (lockErr: unknown) {
52
- // Lock exists — check if stale
53
- if ((lockErr as NodeJS.ErrnoException).code === "EEXIST") {
54
- try {
55
- const lockContent = readFileSync(lockPath, "utf8");
56
- const lock = JSON.parse(lockContent);
57
- const lockAge = Date.now() - new Date(lock.timestamp).getTime();
58
- if (lockAge < LOCK_STALE_MS) return false; // Active lock, skip
59
- // Stale lock — override
60
- writeFileSync(
61
- lockPath,
62
- JSON.stringify({ timestamp: new Date().toISOString(), pid: process.pid }),
63
- );
64
- } catch {
65
- return false; // Can't read lock, skip
66
- }
67
- } else {
68
- return false;
69
- }
70
- }
62
+ // Do not pre-claim the orchestrate lock here. The spawned process must
63
+ // acquire its own lock or it will immediately self-block on startup.
64
+ if (hasFreshOrchestrateLock(lockPath)) return false;
71
65
 
72
66
  // Spawn orchestrate in background (fire-and-forget)
73
67
  try {
74
- const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
75
- stdout: "ignore",
76
- stderr: "ignore",
77
- stdin: "ignore",
78
- });
79
- proc.unref();
68
+ const spawnOrchestrate =
69
+ deps.spawnOrchestrate ??
70
+ (() => {
71
+ const proc = Bun.spawn(["selftune", "orchestrate", "--max-skills", "2"], {
72
+ stdout: "ignore",
73
+ stderr: "ignore",
74
+ stdin: "ignore",
75
+ });
76
+ proc.unref();
77
+ return true;
78
+ });
79
+ if (!spawnOrchestrate()) return false;
80
80
  } catch {
81
- // Spawn failed — release our lock
82
- try {
83
- unlinkSync(lockPath);
84
- } catch {
85
- /* ignore */
86
- }
87
81
  return false;
88
82
  }
89
83
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname } from "node:path";
15
+
15
16
  import { SESSION_STATE_DIR } from "../constants.js";
16
17
  import type { PreToolUsePayload } from "../types.js";
17
18
 
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { basename, dirname } from "node:path";
16
+
16
17
  import { CANONICAL_LOG, SKILL_LOG } from "../constants.js";
17
18
  import {
18
19
  appendCanonicalRecord,
@@ -24,7 +25,6 @@ import {
24
25
  getLatestPromptIdentity,
25
26
  } from "../normalization.js";
26
27
  import type { PostToolUsePayload, SkillUsageRecord } from "../types.js";
27
-
28
28
  import { classifySkillPath } from "../utils/skill-discovery.js";
29
29
  import { getLastUserMessage } from "../utils/transcript.js";
30
30
 
@@ -10,6 +10,7 @@
10
10
  * selftune sync — Sync source-truth telemetry across supported agents
11
11
  * selftune orchestrate — Run autonomous core loop (sync → status → evolve → watch)
12
12
  * selftune init — Initialize agent identity and config
13
+ * selftune uninstall — Clean removal of all selftune data and config
13
14
  * selftune status — Show skill health summary
14
15
  * selftune watch — Monitor post-deploy skill health
15
16
  * selftune doctor — Run health checks
@@ -44,6 +45,7 @@ Commands:
44
45
  sync Sync source-truth telemetry across supported agents
45
46
  orchestrate Run autonomous core loop (sync → status → evolve → watch)
46
47
  init Initialize agent identity and config
48
+ uninstall Clean removal of all selftune data and config
47
49
  status Show skill health summary
48
50
  watch Monitor post-deploy skill health
49
51
  doctor Run health checks
@@ -338,6 +340,11 @@ Run 'selftune eval <action> --help' for action-specific options.`);
338
340
  await cliMain();
339
341
  break;
340
342
  }
343
+ case "uninstall": {
344
+ const { cliMain } = await import("./uninstall.js");
345
+ await cliMain();
346
+ break;
347
+ }
341
348
  case "contribute": {
342
349
  const { cliMain } = await import("./contribute/contribute.js");
343
350
  await cliMain();
@@ -464,7 +471,7 @@ Run 'selftune cron <subcommand> --help' for subcommand-specific options.`);
464
471
  }
465
472
  case "sync": {
466
473
  const { cliMain } = await import("./sync.js");
467
- cliMain();
474
+ await cliMain();
468
475
  break;
469
476
  }
470
477
  case "workflows": {
@@ -606,9 +613,8 @@ Output:
606
613
  const { readAlphaIdentity } = await import("./alpha-identity.js");
607
614
  const { getDb } = await import("./localdb/db.js");
608
615
  const { runUploadCycle } = await import("./alpha-upload/index.js");
609
- const { getSelftuneVersion, readConfiguredAgentType } = await import(
610
- "./utils/selftune-meta.js"
611
- );
616
+ const { getSelftuneVersion, readConfiguredAgentType } =
617
+ await import("./utils/selftune-meta.js");
612
618
 
613
619
  const identity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
614
620
  if (!identity?.enrolled) {
@@ -670,36 +676,39 @@ Output:
670
676
  }
671
677
  case "relink": {
672
678
  const { SELFTUNE_CONFIG_PATH } = await import("./constants.js");
673
- const { readAlphaIdentity, writeAlphaIdentity, generateUserId } = await import(
674
- "./alpha-identity.js"
675
- );
676
- const { requestDeviceCode, pollDeviceCode } = await import("./auth/device-code.js");
679
+ const { readAlphaIdentity, writeAlphaIdentity, generateUserId } =
680
+ await import("./alpha-identity.js");
681
+ const { buildVerificationUrl, pollDeviceCode, requestDeviceCode, tryOpenUrl } =
682
+ await import("./auth/device-code.js");
677
683
  const { chmodSync } = await import("node:fs");
678
684
 
679
685
  const existingIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
680
686
  process.stderr.write("[alpha relink] Starting device-code authentication flow...\n");
681
687
 
682
688
  const grant = await requestDeviceCode();
689
+ const verificationUrlWithCode = buildVerificationUrl(
690
+ grant.verification_url,
691
+ grant.user_code,
692
+ );
683
693
 
684
694
  console.log(
685
695
  JSON.stringify({
686
696
  level: "info",
687
697
  code: "device_code_issued",
688
698
  verification_url: grant.verification_url,
699
+ verification_url_with_code: verificationUrlWithCode,
689
700
  user_code: grant.user_code,
690
701
  expires_in: grant.expires_in,
691
- message: `Open ${grant.verification_url} and enter code: ${grant.user_code}`,
702
+ message: `Open ${verificationUrlWithCode} to approve.`,
692
703
  }),
693
704
  );
694
705
 
695
706
  // Try to open browser
696
- try {
697
- const url = `${grant.verification_url}?code=${grant.user_code}`;
698
- Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
707
+ if (tryOpenUrl(verificationUrlWithCode)) {
699
708
  process.stderr.write("[alpha relink] Browser opened. Waiting for approval...\n");
700
- } catch {
709
+ } else {
701
710
  process.stderr.write(
702
- "[alpha relink] Could not open browser. Visit the URL above manually.\n",
711
+ `[alpha relink] Could not open browser. Visit ${verificationUrlWithCode} manually.\n`,
703
712
  );
704
713
  }
705
714
 
@@ -24,6 +24,7 @@
24
24
  import { statSync } from "node:fs";
25
25
  import { basename } from "node:path";
26
26
  import { parseArgs } from "node:util";
27
+
27
28
  import {
28
29
  CANONICAL_LOG,
29
30
  CLAUDE_CODE_MARKER,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { homedir } from "node:os";
21
21
  import { join } from "node:path";
22
+
22
23
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
23
24
  import {
24
25
  appendCanonicalRecords,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import {
29
30
  CANONICAL_LOG,
30
31
  OPENCLAW_AGENTS_DIR,
@@ -25,6 +25,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { homedir } from "node:os";
26
26
  import { basename, join } from "node:path";
27
27
  import { parseArgs } from "node:util";
28
+
28
29
  import { CANONICAL_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "../constants.js";
29
30
  import {
30
31
  appendCanonicalRecords,