@amistio/cli 0.1.3 → 0.1.5

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/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
4
+ import { createHash as createHash4, randomUUID } from "node:crypto";
5
5
  import { writeFile as writeFile8 } from "node:fs/promises";
6
- import os6 from "node:os";
7
- import path12 from "node:path";
6
+ import os5 from "node:os";
7
+ import path11 from "node:path";
8
8
  import { Command } from "commander";
9
9
 
10
10
  // ../shared/src/schemas.ts
@@ -1204,8 +1204,8 @@ var toolSessionMutationSchema = z3.object({
1204
1204
  });
1205
1205
  function resolveApiUrl(apiUrl, urlPath) {
1206
1206
  const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
1207
- const path13 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1208
- return new URL(`${base}${path13}`);
1207
+ const path12 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
1208
+ return new URL(`${base}${path12}`);
1209
1209
  }
1210
1210
 
1211
1211
  // src/orchestrator.ts
@@ -2645,7 +2645,7 @@ function buildImportedBrainDocuments(options) {
2645
2645
  documentId,
2646
2646
  documentType: candidate.documentType,
2647
2647
  title: candidate.title,
2648
- status: "reviewing",
2648
+ status: "approved",
2649
2649
  repoPath: candidate.repoPath,
2650
2650
  content: candidate.content,
2651
2651
  contentHash: candidate.contentHash,
@@ -2658,10 +2658,10 @@ function buildImportedBrainDocuments(options) {
2658
2658
  },
2659
2659
  revision,
2660
2660
  source: "repo",
2661
- syncState: "dirtyInRepo",
2661
+ syncState: "approved",
2662
2662
  createdAt: existing?.createdAt ?? importedAt,
2663
2663
  updatedAt: importedAt,
2664
- ...existing?.approvedRevision !== void 0 ? { approvedRevision: existing.approvedRevision } : {}
2664
+ approvedRevision: revision
2665
2665
  };
2666
2666
  });
2667
2667
  }
@@ -2880,440 +2880,22 @@ function truncateProcessOutput(value) {
2880
2880
  return trimmed.length > 1200 ? `${trimmed.slice(0, 1200)}...` : trimmed;
2881
2881
  }
2882
2882
 
2883
- // src/runner-tui.ts
2884
- import { randomUUID } from "node:crypto";
2885
- import { readFile as readFile7 } from "node:fs/promises";
2886
- import os5 from "node:os";
2887
- import path11 from "node:path";
2888
- import * as readline from "node:readline";
2889
- async function runRunnerTui(options) {
2890
- const input = options.input ?? process.stdin;
2891
- const output = options.output ?? process.stdout;
2892
- if (!input.isTTY || !output.isTTY) {
2893
- output.write(`${runnerTuiNonInteractiveMessage()}
2894
- `);
2895
- return;
2896
- }
2897
- let selectedIndex = 0;
2898
- let message;
2899
- let state = await loadRunnerTuiState(options);
2900
- const render = async () => {
2901
- state = await loadRunnerTuiState(options);
2902
- selectedIndex = clampSelectedIndex(state, selectedIndex);
2903
- output.write(`\x1B[?25l\x1B[2J\x1B[H${renderRunnerTuiScreen(state, selectedIndex, { columns: output.columns, rows: output.rows, ...message ? { message } : {} })}`);
2904
- };
2905
- readline.emitKeypressEvents(input);
2906
- input.setRawMode?.(true);
2907
- input.resume();
2908
- await render();
2909
- await new Promise((resolve) => {
2910
- let busy = false;
2911
- const keypressHandler = (character, key) => {
2912
- if (busy) return;
2913
- busy = true;
2914
- void handleRunnerTuiKey({ character, input, key, output, options, selectedIndex, state }).then(async (result) => {
2915
- if (result.quit) {
2916
- teardownRunnerTui(input, output, keypressHandler);
2917
- resolve();
2918
- return;
2919
- }
2920
- selectedIndex = result.selectedIndex;
2921
- message = result.message;
2922
- await render();
2923
- }).catch(async (error) => {
2924
- message = error instanceof Error ? error.message : String(error);
2925
- await render();
2926
- }).finally(() => {
2927
- busy = false;
2928
- });
2929
- };
2930
- input.on("keypress", keypressHandler);
2931
- });
2932
- }
2933
- async function loadRunnerTuiState(options) {
2934
- const resolvedRoot = path11.resolve(options.root);
2935
- const metadata = await readProjectLink(resolvedRoot);
2936
- const credentialStore = options.credentialStore ?? new LocalCredentialStore();
2937
- if (!metadata) {
2938
- return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: options.remote, credentialPresent: false, remoteError: "Repository is not paired. Run `amistio pair` or `amistio import <code>` first." });
2939
- }
2940
- const token = await credentialStore.get(credentialKey(metadata.amistioAccountId, metadata.amistioProjectId, metadata.repositoryLinkId));
2941
- const localRecords = await listRunnerDaemonMetadata({
2942
- accountId: metadata.amistioAccountId,
2943
- projectId: metadata.amistioProjectId,
2944
- repositoryLinkId: metadata.repositoryLinkId,
2945
- ...options.runnerId ? { runnerId: options.runnerId } : {}
2946
- }, options.metadataDir);
2947
- if (!options.remote) {
2948
- return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: false, credentialPresent: Boolean(token), metadata, localRecords });
2949
- }
2950
- if (!token) {
2951
- return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: true, credentialPresent: false, metadata, localRecords, remoteError: "Remote runner state was not loaded because this checkout has no local runner credential." });
2952
- }
2953
- const client = new ApiClient({ apiUrl: options.apiUrl, accountId: metadata.amistioAccountId, token });
2954
- try {
2955
- const [{ runners }, preferences] = await Promise.all([
2956
- client.listRunners(metadata.amistioProjectId),
2957
- client.getRunnerPreferences(metadata.amistioProjectId).catch(() => void 0)
2958
- ]);
2959
- const projectRunners = runners.filter((runner2) => runner2.repositoryLinkId === metadata.repositoryLinkId).filter((runner2) => !options.runnerId || runner2.runnerId === options.runnerId);
2960
- const commandGroups = await Promise.all(projectRunners.map((runner2) => client.listRunnerCommands(metadata.amistioProjectId, runner2.runnerId, runner2.repositoryLinkId).then((result) => result.commands).catch(() => [])));
2961
- return createRunnerTuiState({
2962
- root: resolvedRoot,
2963
- apiUrl: options.apiUrl,
2964
- remoteEnabled: true,
2965
- credentialPresent: true,
2966
- metadata,
2967
- localRecords,
2968
- remoteRunners: projectRunners,
2969
- remoteCommands: commandGroups.flat(),
2970
- ...preferences ? { preferences } : {}
2971
- });
2972
- } catch (error) {
2973
- return createRunnerTuiState({ root: resolvedRoot, apiUrl: options.apiUrl, remoteEnabled: true, credentialPresent: true, metadata, localRecords, remoteError: error instanceof Error ? error.message : String(error) });
2974
- }
2975
- }
2976
- function createRunnerTuiState(input) {
2977
- const localRecords = input.localRecords ?? [];
2978
- const remoteRunners = input.remoteRunners ?? [];
2979
- const remoteCommands = input.remoteCommands ?? [];
2980
- const runnersByKey = /* @__PURE__ */ new Map();
2981
- for (const record of localRecords) {
2982
- const runnerKey = runnerKeyFor(record.runnerId, record.repositoryLinkId);
2983
- runnersByKey.set(runnerKey, {
2984
- runnerId: record.runnerId,
2985
- repositoryLinkId: record.repositoryLinkId,
2986
- source: "local",
2987
- local: {
2988
- metadata: record,
2989
- runtimeStatus: runnerDaemonRuntimeStatus(record),
2990
- uptime: runnerDaemonRuntimeStatus(record) === "running" ? runnerDaemonUptime(record, input.nowMs) : "not running"
2991
- }
2992
- });
2993
- }
2994
- for (const heartbeat of remoteRunners) {
2995
- const runnerKey = runnerKeyFor(heartbeat.runnerId, heartbeat.repositoryLinkId);
2996
- const existing = runnersByKey.get(runnerKey);
2997
- const latestCommand = latestRunnerCommand(remoteCommands, heartbeat.runnerId, heartbeat.repositoryLinkId);
2998
- if (existing) {
2999
- runnersByKey.set(runnerKey, { ...existing, heartbeat, ...latestCommand ? { latestCommand } : {} });
3000
- } else {
3001
- runnersByKey.set(runnerKey, { runnerId: heartbeat.runnerId, repositoryLinkId: heartbeat.repositoryLinkId, source: "remote", heartbeat, ...latestCommand ? { latestCommand } : {} });
3002
- }
3003
- }
3004
- const runners = [...runnersByKey.values()].sort((first, second) => {
3005
- if (first.source !== second.source) return first.source === "local" ? -1 : 1;
3006
- return first.runnerId.localeCompare(second.runnerId);
3007
- });
3008
- return {
3009
- root: path11.resolve(input.root),
3010
- apiUrl: input.apiUrl,
3011
- paired: Boolean(input.metadata),
3012
- credentialPresent: input.credentialPresent,
3013
- remoteEnabled: input.remoteEnabled,
3014
- runners,
3015
- localRunnerCount: localRecords.length,
3016
- remoteRunnerCount: remoteRunners.length,
3017
- ...input.metadata ? { metadata: input.metadata } : {},
3018
- ...input.preferences ? { preferences: input.preferences } : {},
3019
- ...input.remoteError ? { remoteError: input.remoteError } : {}
3020
- };
3021
- }
3022
- function renderRunnerTuiScreen(state, selectedIndex = 0, options = {}) {
3023
- const columns = Math.max(40, options.columns ?? 100);
3024
- const rows = Math.max(8, options.rows ?? 32);
3025
- const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
3026
- const lines = [];
3027
- lines.push("Amistio Runner UI");
3028
- lines.push(`Project: ${state.metadata?.amistioProjectId ?? "unpaired"} Repository link: ${state.metadata?.repositoryLinkId ?? "none"}`);
3029
- lines.push(`API: ${state.apiUrl}`);
3030
- lines.push(`Root: ${state.root}`);
3031
- lines.push(`Credential: ${state.credentialPresent ? "available" : "missing"} Remote: ${state.remoteEnabled ? state.remoteError ? "warning" : "enabled" : "local only"}`);
3032
- if (state.remoteError) lines.push(`Remote: ${state.remoteError}`);
3033
- lines.push(`Preferences: ${formatPreferenceSummary(state.preferences)}`);
3034
- lines.push("");
3035
- lines.push(`Runners (${state.localRunnerCount} local, ${state.remoteRunnerCount} remote heartbeat${state.remoteRunnerCount === 1 ? "" : "s"})`);
3036
- if (!state.runners.length) {
3037
- lines.push(" No runner records found. Press s to start a background runner, or run `amistio run --background`.");
3038
- }
3039
- state.runners.forEach((runner2, index) => {
3040
- const marker = index === clampSelectedIndex(state, selectedIndex) ? ">" : " ";
3041
- lines.push(`${marker} ${formatRunnerListLine(runner2)}`);
3042
- });
3043
- lines.push("");
3044
- lines.push("Details");
3045
- if (selectedRunner) {
3046
- lines.push(...formatRunnerDetails(selectedRunner));
3047
- } else {
3048
- lines.push(" No runner selected.");
3049
- }
3050
- lines.push("");
3051
- lines.push("Preference Editing");
3052
- lines.push(" Account/project runner preferences are read-only here until a user-authenticated CLI path exists.");
3053
- const availability = runnerTuiActionAvailability(state, selectedIndex);
3054
- lines.push(`Actions: r refresh | s start${availability.start ? "" : " (needs credential)"} | x stop | b restart | u update CLI | d local remove | l logs | q quit`);
3055
- if (options.message) {
3056
- lines.push("");
3057
- lines.push("Status");
3058
- lines.push(...options.message.split("\n").map((line) => ` ${line}`));
3059
- }
3060
- return `${lines.slice(0, rows).map((line) => fitLine(line, columns)).join("\n")}
3061
- `;
3062
- }
3063
- function runnerTuiActionAvailability(state, selectedIndex = 0) {
3064
- const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
3065
- return {
3066
- start: state.paired && state.credentialPresent,
3067
- stop: Boolean(selectedRunner?.local),
3068
- restart: Boolean(selectedRunner?.local),
3069
- update: state.paired,
3070
- localRemove: state.paired && state.credentialPresent,
3071
- logs: Boolean(selectedRunner?.local?.metadata.logPath)
3072
- };
3073
- }
3074
- function runnerTuiNonInteractiveMessage() {
3075
- return "Terminal UI requires an interactive TTY. Use `amistio runner status` for status or `amistio run --background` to start a background runner.";
3076
- }
3077
- async function handleRunnerTuiKey({ character, input, key, options, output, selectedIndex, state }) {
3078
- if (key.ctrl && key.name === "c" || character === "q") return { quit: true, selectedIndex };
3079
- if (key.name === "up") return { selectedIndex: Math.max(0, selectedIndex - 1) };
3080
- if (key.name === "down") return { selectedIndex: Math.min(Math.max(0, state.runners.length - 1), selectedIndex + 1) };
3081
- if (character === "r") return { selectedIndex, message: "Refreshed runner state." };
3082
- if (character === "s") return { selectedIndex, message: await promptAndStartRunner(state, options, input, output) };
3083
- if (character === "x") return { selectedIndex, message: await stopSelectedRunner(state, selectedIndex, options) };
3084
- if (character === "b") return { selectedIndex, message: await restartSelectedRunner(state, selectedIndex, options) };
3085
- if (character === "u") return { selectedIndex, message: await confirmAndUpdateCli(input, output) };
3086
- if (character === "d") return { selectedIndex, message: await confirmAndRemoveLocalCredential(state, options, input, output) };
3087
- if (character === "l") return { selectedIndex, message: await readSelectedRunnerLog(state, selectedIndex) };
3088
- return { selectedIndex, message: "Unknown key. Use r, s, x, b, u, d, l, or q." };
3089
- }
3090
- async function promptAndStartRunner(state, options, input, output) {
3091
- const context = await loadRunnerTuiContext(state, options);
3092
- if (!context.token) {
3093
- return "Cannot start a background runner because this checkout has no local runner credential.";
3094
- }
3095
- const startOptions = await promptForStartRunnerOptions(input, output, options);
3096
- const metadata = await startRunnerDaemon({
3097
- accountId: context.metadata.amistioAccountId,
3098
- projectId: context.metadata.amistioProjectId,
3099
- repositoryLinkId: context.metadata.repositoryLinkId,
3100
- runnerId: startOptions.runnerId,
3101
- rootDir: path11.resolve(options.root),
3102
- apiUrl: options.apiUrl,
3103
- args: buildBackgroundRunnerArgs({
3104
- apiUrl: options.apiUrl,
3105
- runnerId: startOptions.runnerId,
3106
- root: options.root,
3107
- ...startOptions.tool ? { tool: startOptions.tool } : {},
3108
- ...startOptions.invocationChannel ? { invocationChannel: startOptions.invocationChannel } : {},
3109
- ...startOptions.model ? { model: startOptions.model } : {},
3110
- session: startOptions.session,
3111
- intervalSeconds: startOptions.intervalSeconds,
3112
- stream: startOptions.stream
3113
- }),
3114
- ...options.metadataDir ? { metadataDir: options.metadataDir } : {}
3115
- });
3116
- return `Started background runner ${metadata.runnerId} with PID ${metadata.pid}.${metadata.logPath ? `
3117
- Log: ${metadata.logPath}` : ""}`;
3118
- }
3119
- async function promptForStartRunnerOptions(input, output, options) {
3120
- const runnerId = (await promptLine(input, output, `Runner ID [${options.runnerId ?? "new"}]: `)).trim() || options.runnerId || `runner_${randomUUID()}`;
3121
- const tool = parseOptionalTool(await promptLine(input, output, "AI tool / SDK [remote preference; auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent]: "));
3122
- const invocationChannel = parseOptionalInvocationChannel(await promptLine(input, output, "Invocation channel [remote preference; auto, sdk, command]: "));
3123
- const model = optionalTrim(await promptLine(input, output, "Model [provider default]: "));
3124
- const sessionInput = optionalTrim(await promptLine(input, output, "Session policy [auto]: ")) ?? "auto";
3125
- const session = sessionPolicySchema.parse(sessionInput);
3126
- const intervalInput = optionalTrim(await promptLine(input, output, `Polling interval seconds [${options.intervalSeconds}]: `));
3127
- const intervalSeconds = intervalInput ? parsePositiveInteger(intervalInput) : options.intervalSeconds;
3128
- const streamInput = optionalTrim(await promptLine(input, output, "Stream output from background runner? [Y/n]: "));
3129
- const stream = !streamInput || streamInput.toLowerCase() === "y" || streamInput.toLowerCase() === "yes";
3130
- return { runnerId, ...tool ? { tool } : {}, ...invocationChannel ? { invocationChannel } : {}, ...model ? { model } : {}, session, intervalSeconds, stream };
3131
- }
3132
- async function stopSelectedRunner(state, selectedIndex, options) {
3133
- const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
3134
- if (!selectedRunner?.local) return "Select a local background runner to stop.";
3135
- const stopResult = await stopRunnerDaemonProcess(selectedRunner.local.metadata);
3136
- await markRunnerDaemonStopped(selectedRunner.local.metadata, options.metadataDir);
3137
- const context = await loadRunnerTuiContext(state, options).catch(() => void 0);
3138
- if (context?.token) {
3139
- await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, selectedRunner.runnerId, selectedRunner.repositoryLinkId, "offline", { version: "0.1.3", mode: "background", hostname: os5.hostname() }).catch(() => void 0);
3140
- }
3141
- return stopResult === "stopped" ? `Stopped background runner ${selectedRunner.runnerId}.` : `Marked background runner ${selectedRunner.runnerId} stopped; process was not running.`;
3142
- }
3143
- async function restartSelectedRunner(state, selectedIndex, options) {
3144
- const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
3145
- if (!selectedRunner?.local) return "Select a local background runner to restart.";
3146
- await stopRunnerDaemonProcess(selectedRunner.local.metadata).catch(() => "not-running");
3147
- await markRunnerDaemonStopped(selectedRunner.local.metadata, options.metadataDir);
3148
- const replacement = await restartRunnerDaemonProcess(
3149
- selectedRunner.local.metadata,
3150
- buildBackgroundRunnerArgs({ apiUrl: selectedRunner.local.metadata.apiUrl, runnerId: selectedRunner.runnerId, root: selectedRunner.local.metadata.rootDir, session: "auto", intervalSeconds: options.intervalSeconds, stream: true }),
3151
- { ...options.metadataDir ? { metadataDir: options.metadataDir } : {} }
3152
- );
3153
- return `Restarted background runner ${replacement.runnerId} with PID ${replacement.pid}.`;
3154
- }
3155
- async function confirmAndUpdateCli(input, output) {
3156
- const confirmation = await promptLine(input, output, "Run the official Amistio CLI update now? Type update to continue: ");
3157
- if (confirmation.trim() !== "update") return "Update cancelled.";
3158
- const result = await runOfficialCliUpdate();
3159
- return result.succeeded ? result.message : `${result.message}${result.error ? `
3160
- ${result.error}` : ""}`;
3161
- }
3162
- async function confirmAndRemoveLocalCredential(state, options, input, output) {
3163
- const context = await loadRunnerTuiContext(state, options);
3164
- if (!context.token) return "No local runner credential is stored for this paired repository.";
3165
- const confirmation = await promptLine(input, output, "Remove this machine's runner credential? This does not delete source files, local checkouts, hosted repositories, project records, or team data. Type remove local to continue: ");
3166
- if (confirmation.trim() !== "remove local") return "Local remove cancelled.";
3167
- const localRunners = state.runners.filter((runner2) => runner2.local);
3168
- for (const runner2 of localRunners) {
3169
- await stopRunnerDaemonProcess(runner2.local.metadata).catch(() => "not-running");
3170
- await markRunnerDaemonStopped(runner2.local.metadata, options.metadataDir).catch(() => void 0);
3171
- await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, runner2.runnerId, runner2.repositoryLinkId, "offline", { version: "0.1.3", mode: "background", hostname: os5.hostname() }).catch(() => void 0);
3172
- }
3173
- await context.credentialStore.delete(credentialKey(context.metadata.amistioAccountId, context.metadata.amistioProjectId, context.metadata.repositoryLinkId));
3174
- return `Removed this machine's local runner credential for ${context.metadata.repositoryLinkId}. Source files, hosted repositories, project records, and team data were not deleted.`;
3175
- }
3176
- async function readSelectedRunnerLog(state, selectedIndex) {
3177
- const selectedRunner = state.runners[clampSelectedIndex(state, selectedIndex)];
3178
- const logPath = selectedRunner?.local?.metadata.logPath;
3179
- if (!logPath) return "Selected runner has no local log path.";
3180
- try {
3181
- const content = await readFile7(logPath, "utf8");
3182
- const excerpt = content.trim().slice(-3e3);
3183
- return excerpt ? `Log: ${logPath}
3184
- ${excerpt}` : `Log: ${logPath}
3185
- No log output yet.`;
3186
- } catch (error) {
3187
- return `Could not read ${logPath}: ${error instanceof Error ? error.message : String(error)}`;
3188
- }
3189
- }
3190
- async function loadRunnerTuiContext(state, options) {
3191
- if (!state.metadata) {
3192
- throw new Error("Repository is not paired. Run `amistio pair` or `amistio import <code>` first.");
3193
- }
3194
- const credentialStore = options.credentialStore ?? new LocalCredentialStore();
3195
- const token = await credentialStore.get(credentialKey(state.metadata.amistioAccountId, state.metadata.amistioProjectId, state.metadata.repositoryLinkId));
3196
- return {
3197
- metadata: state.metadata,
3198
- credentialStore,
3199
- ...token ? { token } : {},
3200
- client: new ApiClient({ apiUrl: options.apiUrl, accountId: state.metadata.amistioAccountId, ...token ? { token } : {} })
3201
- };
3202
- }
3203
- function formatRunnerListLine(runner2) {
3204
- const local = runner2.local ? `local ${runner2.local.metadata.status}/${runner2.local.runtimeStatus} pid ${runner2.local.metadata.pid}` : "remote-only";
3205
- const heartbeat = runner2.heartbeat ? `heartbeat ${runner2.heartbeat.status}${runner2.heartbeat.mode ? ` ${runner2.heartbeat.mode}` : ""} ${runner2.heartbeat.lastSeenAt}` : "no heartbeat";
3206
- return `${runner2.runnerId} | ${local} | ${heartbeat}`;
3207
- }
3208
- function formatRunnerDetails(runner2) {
3209
- const lines = [];
3210
- lines.push(` Runner ID: ${runner2.runnerId}`);
3211
- lines.push(` Repository link: ${runner2.repositoryLinkId}`);
3212
- if (runner2.local) {
3213
- lines.push(` Local process: ${runner2.local.runtimeStatus}, PID ${runner2.local.metadata.pid}, uptime ${runner2.local.uptime}`);
3214
- lines.push(` Local root: ${runner2.local.metadata.rootDir}`);
3215
- lines.push(` Host: ${runner2.local.metadata.hostname}`);
3216
- if (runner2.local.metadata.logPath) lines.push(` Log: ${runner2.local.metadata.logPath}`);
3217
- } else {
3218
- lines.push(" Local process: not on this machine");
3219
- }
3220
- if (runner2.heartbeat) {
3221
- const effectiveTool = runner2.heartbeat.effectiveTool ?? runner2.heartbeat.requestedTool ?? "unknown";
3222
- const requestedTool = runner2.heartbeat.requestedTool ?? "auto";
3223
- const channel = runner2.heartbeat.effectiveInvocationChannel ?? runner2.heartbeat.requestedInvocationChannel ?? "auto";
3224
- lines.push(` Last heartbeat: ${runner2.heartbeat.status} at ${runner2.heartbeat.lastSeenAt}${runner2.heartbeat.version ? ` (${runner2.heartbeat.version})` : ""}`);
3225
- lines.push(` Tool: ${requestedTool}${effectiveTool !== requestedTool ? ` -> ${effectiveTool}` : ""}`);
3226
- lines.push(` Channel: ${channel}`);
3227
- lines.push(` Model: ${runner2.heartbeat.effectiveModel ?? "provider default"}`);
3228
- lines.push(` Preference: ${runner2.heartbeat.preferenceSource ?? "default"} / ${runner2.heartbeat.preferenceStatus ?? "pending"}`);
3229
- if (runner2.heartbeat.preferenceMessage) lines.push(` Warning: ${runner2.heartbeat.preferenceMessage}`);
3230
- lines.push(` Capabilities: ${formatCapabilities(runner2.heartbeat)}`);
3231
- } else {
3232
- lines.push(" Last heartbeat: not loaded");
3233
- }
3234
- if (runner2.latestCommand) {
3235
- lines.push(` Latest command: ${runner2.latestCommand.commandKind} ${runner2.latestCommand.status}${runner2.latestCommand.message ? ` - ${runner2.latestCommand.message}` : ""}`);
3236
- } else {
3237
- lines.push(" Latest command: none loaded");
3238
- }
3239
- return lines;
3240
- }
3241
- function formatCapabilities(runner2) {
3242
- const availableCapabilities = runner2.capabilities?.filter((capability) => capability.available) ?? [];
3243
- if (!availableCapabilities.length) return "unknown";
3244
- return availableCapabilities.map((capability) => `${capability.name} (${capability.sdkAvailable && capability.commandAvailable ? "sdk+command" : capability.sdkAvailable ? "sdk" : capability.commandAvailable ? "command" : capability.execution})`).join(", ");
3245
- }
3246
- function formatPreferenceSummary(preferences) {
3247
- if (!preferences) return "not loaded";
3248
- const effective = preferences.effective;
3249
- return `${effective.source}: ${effective.tool} / ${effective.invocationChannel} / ${effective.model ?? "provider default"}${formatSettingsSuffix(preferences.project, "project")}${formatSettingsSuffix(preferences.account, "account")}`;
3250
- }
3251
- function formatSettingsSuffix(settings, label) {
3252
- if (!settings) return "";
3253
- const preference = settings.preferences;
3254
- return `; ${label} ${preference.tool ?? "unset"}/${preference.invocationChannel ?? "unset"}/${preference.model ?? "provider default"}`;
3255
- }
3256
- function latestRunnerCommand(commands, runnerId, repositoryLinkId) {
3257
- return commands.filter((command) => command.runnerId === runnerId && command.repositoryLinkId === repositoryLinkId).sort((first, second) => Date.parse(second.createdAt) - Date.parse(first.createdAt))[0];
3258
- }
3259
- function runnerKeyFor(runnerId, repositoryLinkId) {
3260
- return `${repositoryLinkId}:${runnerId}`;
3261
- }
3262
- function clampSelectedIndex(state, selectedIndex) {
3263
- if (!state.runners.length) return 0;
3264
- return Math.max(0, Math.min(state.runners.length - 1, selectedIndex));
3265
- }
3266
- function fitLine(line, columns) {
3267
- if (line.length <= columns) return line;
3268
- return `${line.slice(0, Math.max(0, columns - 3))}...`;
3269
- }
3270
- function parseOptionalTool(value) {
3271
- const trimmed = optionalTrim(value);
3272
- if (!trimmed) return void 0;
3273
- if (trimmed === "auto" || trimmed === "none" || runnerToolNames.includes(trimmed)) {
3274
- return trimmed;
2883
+ // src/version.ts
2884
+ import { readFileSync } from "node:fs";
2885
+ function readCliPackageVersion() {
2886
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
2887
+ if (typeof packageJson.version !== "string" || !packageJson.version.trim()) {
2888
+ throw new Error("@amistio/cli package version is missing.");
3275
2889
  }
3276
- throw new Error(`Expected auto, none, ${runnerToolNames.join(", ")}; received ${trimmed}.`);
3277
- }
3278
- function parseOptionalInvocationChannel(value) {
3279
- const trimmed = optionalTrim(value);
3280
- if (!trimmed) return void 0;
3281
- return runnerInvocationChannelSchema.parse(trimmed);
3282
- }
3283
- function parsePositiveInteger(value) {
3284
- const parsed = Number(value);
3285
- if (!Number.isInteger(parsed) || parsed <= 0) {
3286
- throw new Error(`Expected a positive integer, received ${value}.`);
3287
- }
3288
- return parsed;
3289
- }
3290
- function optionalTrim(value) {
3291
- const trimmed = value.trim();
3292
- return trimmed ? trimmed : void 0;
3293
- }
3294
- function promptLine(input, output, question) {
3295
- input.setRawMode?.(false);
3296
- return new Promise((resolve) => {
3297
- const prompt = readline.createInterface({ input, output });
3298
- prompt.question(question, (answer) => {
3299
- prompt.close();
3300
- input.setRawMode?.(true);
3301
- resolve(answer);
3302
- });
3303
- });
3304
- }
3305
- function teardownRunnerTui(input, output, keypressHandler) {
3306
- input.setRawMode?.(false);
3307
- input.off("keypress", keypressHandler);
3308
- output.write("\x1B[?25h\n");
2890
+ return packageJson.version.trim();
3309
2891
  }
2892
+ var CLI_VERSION = readCliPackageVersion();
3310
2893
 
3311
2894
  // src/index.ts
3312
2895
  var program = new Command();
3313
2896
  var defaultRoot = process.env.INIT_CWD ?? process.cwd();
3314
2897
  var apiUrlOptionDescription = `Amistio API URL override (or ${AMISTIO_API_URL_ENV})`;
3315
- program.name("amistio").description("Amistio project brain CLI").version("0.1.3");
3316
- var CLI_VERSION = "0.1.3";
2898
+ program.name("amistio").description("Amistio project brain CLI").version(CLI_VERSION);
3317
2899
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
3318
2900
  const created = await initControlPlane(options.root);
3319
2901
  console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
@@ -3354,7 +2936,7 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
3354
2936
  console.log(`Wrote non-secret project metadata to ${filePath}.`);
3355
2937
  console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
3356
2938
  });
3357
- program.command("import").description("Pair an existing checkout and import legacy Markdown docs for review").argument("[code]", "Short-lived pairing code from the Amistio app").option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--root <path>", "Repository root", defaultRoot).option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--default-branch <branch>", "Default branch fallback", "main").option("--include <glob>", "Only import files matching a repo-relative glob", collectRepeatedOption, []).option("--exclude <glob>", "Exclude files matching a repo-relative glob", collectRepeatedOption, []).option("--max-file-kb <kb>", "Maximum Markdown file size to import", parsePositiveInteger2, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
2939
+ program.command("import").description("Pair an existing checkout and import legacy Markdown docs as accepted brain records").argument("[code]", "Short-lived pairing code from the Amistio app").option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--root <path>", "Repository root", defaultRoot).option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--default-branch <branch>", "Default branch fallback", "main").option("--include <glob>", "Only import files matching a repo-relative glob", collectRepeatedOption, []).option("--exclude <glob>", "Exclude files matching a repo-relative glob", collectRepeatedOption, []).option("--max-file-kb <kb>", "Maximum Markdown file size to import", parsePositiveInteger, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
3358
2940
  const pairingCode = (options.pairingCode ?? code)?.trim();
3359
2941
  if (!pairingCode) {
3360
2942
  throw new Error("Provide a pairing code as `amistio import <code>` or with `--pairing-code <code>`.");
@@ -3419,11 +3001,11 @@ program.command("import").description("Pair an existing checkout and import lega
3419
3001
  }
3420
3002
  console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}; repository link ${pairing.repositoryLinkAction}.`);
3421
3003
  console.log(`Wrote non-secret project metadata to ${metadataFilePath}.`);
3422
- console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"} for review.`);
3004
+ console.log(`Imported ${documents.length} legacy document${documents.length === 1 ? "" : "s"} as accepted brain record${documents.length === 1 ? "" : "s"}.`);
3423
3005
  console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
3424
3006
  });
3425
3007
  program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
3426
- let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID2()}`;
3008
+ let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
3427
3009
  let credential = options.token;
3428
3010
  if (options.pairingCode) {
3429
3011
  const pairing = await new ApiClient({
@@ -3605,7 +3187,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
3605
3187
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
3606
3188
  ...options.model ? { model: options.model } : {},
3607
3189
  streamOutput: options.stream,
3608
- ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
3190
+ ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
3609
3191
  });
3610
3192
  if (!options.stream && result.stdout.trim()) {
3611
3193
  console.log(result.stdout.trim());
@@ -3617,7 +3199,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
3617
3199
  process.exitCode = result.exitCode;
3618
3200
  }
3619
3201
  });
3620
- program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID2()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger2, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger2).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
3202
+ program.command("run").description("Claim and run approved Amistio work locally").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--runner-id <runnerId>", "Stable runner ID", `runner_${randomUUID()}`).option("--root <path>", "Repository root", defaultRoot).option("--tool <name>", "Local tool to use: auto, none, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--tool-command <command>", "Custom local command. Use {promptFile} and {root} placeholders when supported").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--dry-run", "Claim work and print the generated execution prompt without running a tool").option("--watch", "Keep polling for approved work until stopped").option("--background", "Start a detached background runner that watches for approved work").option("--interval-seconds <seconds>", "Polling interval for --watch", parsePositiveInteger, 10).option("--max-iterations <count>", "Stop watch mode after this many polling attempts", parsePositiveInteger).option("--no-stream", "Capture local tool output instead of streaming it").action(async (options, command) => {
3621
3203
  const context = await loadPairedApiContext(options.root, options.apiUrl);
3622
3204
  if (!context) {
3623
3205
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -3639,7 +3221,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
3639
3221
  projectId: context.metadata.amistioProjectId,
3640
3222
  repositoryLinkId: context.metadata.repositoryLinkId,
3641
3223
  runnerId: options.runnerId,
3642
- rootDir: path12.resolve(options.root),
3224
+ rootDir: path11.resolve(options.root),
3643
3225
  apiUrl: options.apiUrl,
3644
3226
  args: buildBackgroundRunnerArgs(options)
3645
3227
  });
@@ -3706,9 +3288,6 @@ program.command("run").description("Claim and run approved Amistio work locally"
3706
3288
  }
3707
3289
  });
3708
3290
  var runner = program.command("runner").description("Manage local Amistio runner processes");
3709
- runner.command("ui").alias("tui").description("Open an interactive terminal UI for local runner management").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Select or prefill a runner ID").option("--interval-seconds <seconds>", "Default polling interval for started background runners", parsePositiveInteger2, 10).option("--no-remote", "Skip remote API calls and show local runner state only").action(async (options) => {
3710
- await runRunnerTui(options);
3711
- });
3712
3291
  runner.command("status").description("Show background runner status for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Limit status to one runner ID").action(async (options) => {
3713
3292
  const context = await loadPairedApiContext(options.root, options.apiUrl);
3714
3293
  if (!context) {
@@ -3912,7 +3491,7 @@ async function runNextWorkItem({
3912
3491
  projectId,
3913
3492
  result.workItem.workItemId,
3914
3493
  finalStatus,
3915
- `run_${result.workItem.workItemId}_${randomUUID2()}`,
3494
+ `run_${result.workItem.workItemId}_${randomUUID()}`,
3916
3495
  runnerId,
3917
3496
  {
3918
3497
  tool: preview.toolName,
@@ -3958,7 +3537,7 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
3958
3537
  runnerId: context.runnerId,
3959
3538
  repositoryLinkId: context.repositoryLinkId,
3960
3539
  status,
3961
- idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
3540
+ idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID()}`,
3962
3541
  message,
3963
3542
  ...error ? { error } : {}
3964
3543
  });
@@ -4043,7 +3622,7 @@ ${toolResult.stderr}`);
4043
3622
  const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
4044
3623
  status: "completed",
4045
3624
  runnerId,
4046
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
3625
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
4047
3626
  artifacts,
4048
3627
  tool: toolName,
4049
3628
  durationMs,
@@ -4057,7 +3636,7 @@ ${toolResult.stderr}`);
4057
3636
  await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
4058
3637
  status: "failed",
4059
3638
  runnerId,
4060
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
3639
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
4061
3640
  tool: toolName,
4062
3641
  durationMs,
4063
3642
  ...sessionTelemetry,
@@ -4173,7 +3752,7 @@ async function prepareToolSession({
4173
3752
  });
4174
3753
  return { ...selection, toolSession: toolSession2 };
4175
3754
  }
4176
- const toolSessionId = `tool_session_${randomUUID2()}`;
3755
+ const toolSessionId = `tool_session_${randomUUID()}`;
4177
3756
  const { toolSession } = await apiClient.createToolSession(projectId, {
4178
3757
  toolSessionId,
4179
3758
  repositoryLinkId,
@@ -4261,7 +3840,7 @@ function collectRepeatedOption(value, previous) {
4261
3840
  function formatImportSkipSummary(counts) {
4262
3841
  return `Skipped: ${counts.excluded} excluded, ${counts.tooLarge} too large, ${counts.alreadyManaged} already managed, ${counts.unreadable} unreadable, ${counts.notMarkdown} non-Markdown.`;
4263
3842
  }
4264
- function parsePositiveInteger2(value) {
3843
+ function parsePositiveInteger(value) {
4265
3844
  const parsed = Number(value);
4266
3845
  if (!Number.isInteger(parsed) || parsed <= 0) {
4267
3846
  throw new Error(`Expected a positive integer, received ${value}.`);
@@ -4275,7 +3854,7 @@ function parseInvocationChannel(value) {
4275
3854
  throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
4276
3855
  }
4277
3856
  function inferRepoName(root) {
4278
- return path12.basename(path12.resolve(root)) || "repository";
3857
+ return path11.basename(path11.resolve(root)) || "repository";
4279
3858
  }
4280
3859
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
4281
3860
  return createHash4("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
@@ -4399,7 +3978,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
4399
3978
  return {
4400
3979
  version: CLI_VERSION,
4401
3980
  mode,
4402
- hostname: os6.hostname(),
3981
+ hostname: os5.hostname(),
4403
3982
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
4404
3983
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
4405
3984
  ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},