@amistio/cli 0.1.3 → 0.1.4

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
@@ -2880,440 +2880,12 @@ 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;
3275
- }
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");
3309
- }
3310
-
3311
2883
  // src/index.ts
3312
2884
  var program = new Command();
3313
2885
  var defaultRoot = process.env.INIT_CWD ?? process.cwd();
3314
2886
  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";
2887
+ program.name("amistio").description("Amistio project brain CLI").version("0.1.4");
2888
+ var CLI_VERSION = "0.1.4";
3317
2889
  program.command("init").description("Create Amistio control-plane folders for a new project").option("--root <path>", "Repository root", defaultRoot).action(async (options) => {
3318
2890
  const created = await initControlPlane(options.root);
3319
2891
  console.log(created.length ? `Created ${created.length} control-plane folders.` : "Control-plane folders already exist.");
@@ -3354,7 +2926,7 @@ program.command("bootstrap").description("Clone a linked repository locally, pre
3354
2926
  console.log(`Wrote non-secret project metadata to ${filePath}.`);
3355
2927
  console.log(`Next: cd ${formatShellArg(checkout.targetDir)} && amistio run${formatApiUrlFlag(options.apiUrl)} --watch`);
3356
2928
  });
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) => {
2929
+ 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", parsePositiveInteger, 256).option("--dry-run", "Inspect and print import candidates without consuming the code or uploading documents").action(async (code, options) => {
3358
2930
  const pairingCode = (options.pairingCode ?? code)?.trim();
3359
2931
  if (!pairingCode) {
3360
2932
  throw new Error("Provide a pairing code as `amistio import <code>` or with `--pairing-code <code>`.");
@@ -3423,7 +2995,7 @@ program.command("import").description("Pair an existing checkout and import lega
3423
2995
  console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
3424
2996
  });
3425
2997
  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()}`;
2998
+ let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
3427
2999
  let credential = options.token;
3428
3000
  if (options.pairingCode) {
3429
3001
  const pairing = await new ApiClient({
@@ -3605,7 +3177,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
3605
3177
  ...options.toolCommand ? { toolCommand: options.toolCommand } : {},
3606
3178
  ...options.model ? { model: options.model } : {},
3607
3179
  streamOutput: options.stream,
3608
- ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID2()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
3180
+ ...sessionPolicy === "none" ? {} : { session: { toolSessionId: `local_orchestration_${randomUUID()}`, policy: sessionPolicy, decision: localSessionDecision(sessionPolicy) } }
3609
3181
  });
3610
3182
  if (!options.stream && result.stdout.trim()) {
3611
3183
  console.log(result.stdout.trim());
@@ -3617,7 +3189,7 @@ program.command("orchestrate").description("Update the Amistio control plane thr
3617
3189
  process.exitCode = result.exitCode;
3618
3190
  }
3619
3191
  });
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) => {
3192
+ 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
3193
  const context = await loadPairedApiContext(options.root, options.apiUrl);
3622
3194
  if (!context) {
3623
3195
  console.log("Repository is not paired. Run `amistio pair` first.");
@@ -3639,7 +3211,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
3639
3211
  projectId: context.metadata.amistioProjectId,
3640
3212
  repositoryLinkId: context.metadata.repositoryLinkId,
3641
3213
  runnerId: options.runnerId,
3642
- rootDir: path12.resolve(options.root),
3214
+ rootDir: path11.resolve(options.root),
3643
3215
  apiUrl: options.apiUrl,
3644
3216
  args: buildBackgroundRunnerArgs(options)
3645
3217
  });
@@ -3706,9 +3278,6 @@ program.command("run").description("Claim and run approved Amistio work locally"
3706
3278
  }
3707
3279
  });
3708
3280
  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
3281
  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
3282
  const context = await loadPairedApiContext(options.root, options.apiUrl);
3714
3283
  if (!context) {
@@ -3912,7 +3481,7 @@ async function runNextWorkItem({
3912
3481
  projectId,
3913
3482
  result.workItem.workItemId,
3914
3483
  finalStatus,
3915
- `run_${result.workItem.workItemId}_${randomUUID2()}`,
3484
+ `run_${result.workItem.workItemId}_${randomUUID()}`,
3916
3485
  runnerId,
3917
3486
  {
3918
3487
  tool: preview.toolName,
@@ -3958,7 +3527,7 @@ async function updateRunnerCommandStatus(apiClient, context, command, status, me
3958
3527
  runnerId: context.runnerId,
3959
3528
  repositoryLinkId: context.repositoryLinkId,
3960
3529
  status,
3961
- idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID2()}`,
3530
+ idempotencyKey: `runner_command_${command.commandId}_${status}_${randomUUID()}`,
3962
3531
  message,
3963
3532
  ...error ? { error } : {}
3964
3533
  });
@@ -4043,7 +3612,7 @@ ${toolResult.stderr}`);
4043
3612
  const result = await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
4044
3613
  status: "completed",
4045
3614
  runnerId,
4046
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
3615
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
4047
3616
  artifacts,
4048
3617
  tool: toolName,
4049
3618
  durationMs,
@@ -4057,7 +3626,7 @@ ${toolResult.stderr}`);
4057
3626
  await apiClient.submitBrainGenerationResult(projectId, workItem.workItemId, {
4058
3627
  status: "failed",
4059
3628
  runnerId,
4060
- idempotencyKey: `generation_${workItem.workItemId}_${randomUUID2()}`,
3629
+ idempotencyKey: `generation_${workItem.workItemId}_${randomUUID()}`,
4061
3630
  tool: toolName,
4062
3631
  durationMs,
4063
3632
  ...sessionTelemetry,
@@ -4173,7 +3742,7 @@ async function prepareToolSession({
4173
3742
  });
4174
3743
  return { ...selection, toolSession: toolSession2 };
4175
3744
  }
4176
- const toolSessionId = `tool_session_${randomUUID2()}`;
3745
+ const toolSessionId = `tool_session_${randomUUID()}`;
4177
3746
  const { toolSession } = await apiClient.createToolSession(projectId, {
4178
3747
  toolSessionId,
4179
3748
  repositoryLinkId,
@@ -4261,7 +3830,7 @@ function collectRepeatedOption(value, previous) {
4261
3830
  function formatImportSkipSummary(counts) {
4262
3831
  return `Skipped: ${counts.excluded} excluded, ${counts.tooLarge} too large, ${counts.alreadyManaged} already managed, ${counts.unreadable} unreadable, ${counts.notMarkdown} non-Markdown.`;
4263
3832
  }
4264
- function parsePositiveInteger2(value) {
3833
+ function parsePositiveInteger(value) {
4265
3834
  const parsed = Number(value);
4266
3835
  if (!Number.isInteger(parsed) || parsed <= 0) {
4267
3836
  throw new Error(`Expected a positive integer, received ${value}.`);
@@ -4275,7 +3844,7 @@ function parseInvocationChannel(value) {
4275
3844
  throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
4276
3845
  }
4277
3846
  function inferRepoName(root) {
4278
- return path12.basename(path12.resolve(root)) || "repository";
3847
+ return path11.basename(path11.resolve(root)) || "repository";
4279
3848
  }
4280
3849
  function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
4281
3850
  return createHash4("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
@@ -4399,7 +3968,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
4399
3968
  return {
4400
3969
  version: CLI_VERSION,
4401
3970
  mode,
4402
- hostname: os6.hostname(),
3971
+ hostname: os5.hostname(),
4403
3972
  ...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
4404
3973
  ...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
4405
3974
  ...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},