directionallylocal 0.5.0 → 0.6.0
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/cli.js +4 -0
- package/dist/laptop/commands.js +10 -32
- package/dist/laptop/session.js +3 -2
- package/dist/laptop/settings.js +193 -0
- package/dist/laptop/setup.js +18 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { briefStatus, connectLaptop, laptopsCommand, logsCommand, pairLaptop, se
|
|
|
7
7
|
import { cancel, connect, describeError, laptopHome, pull, run, runs, servers, use, watch } from "./laptop/commands.js";
|
|
8
8
|
import { loadRemotes } from "./laptop/config.js";
|
|
9
9
|
import { keys } from "./laptop/keys.js";
|
|
10
|
+
import { model } from "./laptop/settings.js";
|
|
10
11
|
import { projectSession } from "./laptop/session.js";
|
|
11
12
|
import { setup } from "./laptop/setup.js";
|
|
12
13
|
import { localCommand } from "./local/cli.js";
|
|
@@ -36,6 +37,7 @@ On your laptop:
|
|
|
36
37
|
dl servers where agents run (dl servers remove NAME · dl servers rename OLD NEW)
|
|
37
38
|
dl use [NAME] where tasks go without --server (no NAME: show it)
|
|
38
39
|
dl keys [openrouter|github] which keys are set, or replace one (--server NAME)
|
|
40
|
+
dl model [ID] the model for tasks and sessions (no ID: pick from a list)
|
|
39
41
|
run options: --server NAME, -m/--model MODEL, --repo OWNER/NAME, --detach
|
|
40
42
|
dl local status agents running on this computer: running?, where, how many fit
|
|
41
43
|
dl local uninstall [--purge] stop and remove them (--purge also deletes keys and data)
|
|
@@ -293,6 +295,8 @@ export async function main(argv, io = {
|
|
|
293
295
|
return await servers(parsed.positionals.slice(1), laptopIO);
|
|
294
296
|
case "use":
|
|
295
297
|
return await use(parsed.positionals.slice(1), laptopIO);
|
|
298
|
+
case "model":
|
|
299
|
+
return await model(parsed.positionals.slice(1), { io: laptopIO, terminal });
|
|
296
300
|
case "runs":
|
|
297
301
|
// On a server, its runs; on a laptop, the tasks it sent.
|
|
298
302
|
if (!(await readState(system))) {
|
package/dist/laptop/commands.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import { homedir, hostname } from "node:os";
|
|
3
|
+
import { hostname } from "node:os";
|
|
5
4
|
import { promisify } from "node:util";
|
|
6
5
|
import { isModelNotPriced } from "../model-prices.js";
|
|
7
6
|
import { palette } from "../ui.js";
|
|
8
7
|
import { RemoteApiError, RemoteClient, TERMINAL_STATUSES } from "./client.js";
|
|
9
8
|
import { defaultRemoteName, loadRemotes, loadRemotesFile, normalizeServerUrl, recordLastUsed, REMOTE_NAME_RE, removeRemote, renameRemote, resolveRemote, resolveTarget, saveRemote, setDefaultRemote, } from "./config.js";
|
|
10
|
-
import { autopilotConfigPath } from "./credentials.js";
|
|
11
9
|
import { PAIRED_NAME } from "../local/layout.js";
|
|
12
10
|
import { detectGitHubRepository, snapshotWorkingTree } from "./git.js";
|
|
11
|
+
import { DEFAULT_MODEL, laptopModel } from "./settings.js";
|
|
13
12
|
import { sshCommandLine } from "./ssh.js";
|
|
14
13
|
import { openBrowser } from "./open-browser.js";
|
|
15
14
|
import { withConnections } from "./tunnel.js";
|
|
15
|
+
export { DEFAULT_MODEL, readAutopilotModel } from "./settings.js";
|
|
16
16
|
/**
|
|
17
17
|
* `dl` on a laptop: send tasks to where agents run (this computer, or your own server),
|
|
18
18
|
* follow them, and check out the pull requests. It talks to the server's API directly (the
|
|
@@ -23,8 +23,6 @@ import { withConnections } from "./tunnel.js";
|
|
|
23
23
|
*/
|
|
24
24
|
const execFileAsync = promisify(execFile);
|
|
25
25
|
const RUN_END_EVENTS = new Set(["completed", "failed", "cancelled", "interrupted"]);
|
|
26
|
-
/** The model a task gets when neither -m nor the laptop's Autopilot picks one the server offers. */
|
|
27
|
-
export const DEFAULT_MODEL = "moonshotai/kimi-k2.6";
|
|
28
26
|
const processIO = {
|
|
29
27
|
out: (text) => process.stdout.write(text),
|
|
30
28
|
err: (text) => process.stderr.write(text),
|
|
@@ -68,7 +66,7 @@ export async function connect(input, io = processIO) {
|
|
|
68
66
|
*/
|
|
69
67
|
export async function run(input, io = processIO) {
|
|
70
68
|
const { remote, reason } = await resolveTarget(input.server);
|
|
71
|
-
const preferred = input.preferredModel ?? await (input.autopilotModel ??
|
|
69
|
+
const preferred = input.preferredModel ?? await (input.autopilotModel ?? laptopModel)();
|
|
72
70
|
return withConnections(input.open, async (connections) => runOn(await connections.client(remote), remote, reason, { ...input, ...(preferred ? { preferredModel: preferred } : {}) }, io));
|
|
73
71
|
}
|
|
74
72
|
async function runOn(client, remote, reason, input, io) {
|
|
@@ -264,7 +262,7 @@ export async function renameServer(args, io = processIO, path) {
|
|
|
264
262
|
return 0;
|
|
265
263
|
}
|
|
266
264
|
/** `dl` on a laptop: what this is, which servers it's connected to, what to type next. */
|
|
267
|
-
export async function laptopHome(io = processIO, open) {
|
|
265
|
+
export async function laptopHome(io = processIO, open, model = laptopModel) {
|
|
268
266
|
const c = palette(io.color === true);
|
|
269
267
|
const remotes = await loadRemotes();
|
|
270
268
|
if (remotes.length === 0)
|
|
@@ -278,11 +276,12 @@ export async function laptopHome(io = processIO, open) {
|
|
|
278
276
|
total += list?.length ?? 0;
|
|
279
277
|
}
|
|
280
278
|
});
|
|
281
|
-
const current = await defaultName();
|
|
279
|
+
const [current, chosen] = await Promise.all([defaultName(), model()]);
|
|
282
280
|
io.out(`\n ${c.bold("Directionally Local")} ${c.dim("on this laptop")}\n\n`);
|
|
283
281
|
io.out(" Coding agents that run on this computer or your own server: send a task, get a pull request back.\n\n");
|
|
284
282
|
io.out(` ${c.dim("Runs on".padEnd(11))}${remotes.map((remote) => `${mark(remote, current, c)} ${remote.name} ${c.dim(where(remote))}`).join(", ")} ${c.dim(remotes.length > 1 ? "dl servers · dl use NAME" : "dl servers")}\n`);
|
|
285
|
-
io.out(` ${c.dim("Tasks".padEnd(11))}${running} running · ${total} in all ${c.dim("dl runs")}\n
|
|
283
|
+
io.out(` ${c.dim("Tasks".padEnd(11))}${running} running · ${total} in all ${c.dim("dl runs")}\n`);
|
|
284
|
+
io.out(` ${c.dim("Model".padEnd(11))}${chosen ?? DEFAULT_MODEL} ${c.dim("dl model")}\n\n`);
|
|
286
285
|
io.out(` ${c.dim("Start a session: cd into a GitHub repository, then type")} ${c.bold("dl")}\n`);
|
|
287
286
|
io.out(` ${c.dim("Or hand off a one-off task:")} ${c.bold('dl run "fix the flaky checkout test"')}\n\n`);
|
|
288
287
|
return true;
|
|
@@ -402,7 +401,7 @@ async function findRun(id, server, connections) {
|
|
|
402
401
|
return matches[0];
|
|
403
402
|
}
|
|
404
403
|
/**
|
|
405
|
-
* The task's model: -m/--model; else the laptop's
|
|
404
|
+
* The task's model: -m/--model; else the laptop's default (dl model, else Autopilot's), if the server offers it;
|
|
406
405
|
* else DEFAULT_MODEL, if offered. Never just the first in the server's list (that's the
|
|
407
406
|
* whole OpenRouter catalog, in no useful order).
|
|
408
407
|
*/
|
|
@@ -419,30 +418,9 @@ export function pickModel(models, requested, preferred) {
|
|
|
419
418
|
return DEFAULT_MODEL;
|
|
420
419
|
if (models.length === 0)
|
|
421
420
|
throw new Error("The server offers no models; check its OpenRouter key (on the server: dl doctor)");
|
|
422
|
-
const tried = preferred && preferred !== DEFAULT_MODEL ? `your
|
|
421
|
+
const tried = preferred && preferred !== DEFAULT_MODEL ? `your model (${preferred}) or ${DEFAULT_MODEL}` : DEFAULT_MODEL;
|
|
423
422
|
throw new Error(`The server doesn't offer ${tried}. ${how}`);
|
|
424
423
|
}
|
|
425
|
-
/**
|
|
426
|
-
* The laptop's Autopilot default model: KIMI_MODEL (Autopilot's own override) or the
|
|
427
|
-
* `model` in its config. undefined when neither is set or the config can't be read.
|
|
428
|
-
*/
|
|
429
|
-
export async function readAutopilotModel(sources = {}) {
|
|
430
|
-
const env = sources.env ?? process.env;
|
|
431
|
-
const fromEnv = env.KIMI_MODEL?.trim();
|
|
432
|
-
if (fromEnv)
|
|
433
|
-
return fromEnv;
|
|
434
|
-
const read = sources.readFile ?? ((path) => readFile(path, "utf8").catch(() => undefined));
|
|
435
|
-
const raw = await read(autopilotConfigPath(env, sources.home ?? homedir()));
|
|
436
|
-
if (!raw)
|
|
437
|
-
return undefined;
|
|
438
|
-
try {
|
|
439
|
-
const model = JSON.parse(raw).model;
|
|
440
|
-
return typeof model === "string" && model.trim() ? model.trim() : undefined;
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
return undefined;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
424
|
/** Where a place is, as people know it: this computer, its URL, or the SSH destination its tunnel goes to. */
|
|
447
425
|
function where(remote) {
|
|
448
426
|
if (remote.ssh)
|
package/dist/laptop/session.js
CHANGED
|
@@ -7,7 +7,8 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { palette } from "../ui.js";
|
|
9
9
|
import { RemoteApiError } from "./client.js";
|
|
10
|
-
import { describeError, pickModel
|
|
10
|
+
import { describeError, pickModel } from "./commands.js";
|
|
11
|
+
import { laptopModel } from "./settings.js";
|
|
11
12
|
import { recordLastUsed, resolveTarget } from "./config.js";
|
|
12
13
|
import { detectGitHubRepository, snapshotWorkingTree } from "./git.js";
|
|
13
14
|
import { findSession, forgetSession, rememberSession, sessionsPath } from "./session-store.js";
|
|
@@ -121,7 +122,7 @@ export async function projectSession(input, io) {
|
|
|
121
122
|
const [workspaces, models, preferred] = await step(Promise.all([
|
|
122
123
|
client.workspaces(),
|
|
123
124
|
client.models(),
|
|
124
|
-
input.model ? Promise.resolve(undefined) : (input.autopilotModel ??
|
|
125
|
+
input.model ? Promise.resolve(undefined) : (input.autopilotModel ?? laptopModel)(),
|
|
125
126
|
]));
|
|
126
127
|
const workspace = workspaces[0];
|
|
127
128
|
if (!workspace)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fetchModelPrices } from "../model-prices.js";
|
|
5
|
+
import { PromptCancelled, TtyPrompter } from "../prompt.js";
|
|
6
|
+
import { palette, PlainUi, TerminalUi } from "../ui.js";
|
|
7
|
+
import { autopilotConfigPath } from "./credentials.js";
|
|
8
|
+
/**
|
|
9
|
+
* dl's own settings on this laptop (~/.config/directionallylocal/config.json). Today only
|
|
10
|
+
* the model tasks and sessions use when none is named with -m. It's dl's, so people who
|
|
11
|
+
* never installed Autopilot can set it; Autopilot's own default is still honoured when
|
|
12
|
+
* dl has none.
|
|
13
|
+
*/
|
|
14
|
+
/** What tasks and sessions use when neither -m, dl model nor Autopilot names a model. */
|
|
15
|
+
export const DEFAULT_MODEL = "deepseek/deepseek-v4.1-flash-1017";
|
|
16
|
+
/** The suggestions setup and `dl model` offer first; any OpenRouter ID works too. */
|
|
17
|
+
export const SUGGESTED_MODELS = [
|
|
18
|
+
{ id: "deepseek/deepseek-v4.1-flash-1017", note: "fast and cheap" },
|
|
19
|
+
{ id: "moonshotai/kimi-k2.6", note: "strong at agentic coding" },
|
|
20
|
+
{ id: "anthropic/claude-sonnet-5.5", note: "stronger, pricier" },
|
|
21
|
+
{ id: "openai/gpt-6.1-sol", note: "stronger, pricier" },
|
|
22
|
+
];
|
|
23
|
+
export const OPENROUTER_ID_EXAMPLE = "provider/model, e.g. z-ai/glm-5";
|
|
24
|
+
export function settingsPath(env = process.env) {
|
|
25
|
+
const configHome = env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
26
|
+
return join(configHome, "directionallylocal", "config.json");
|
|
27
|
+
}
|
|
28
|
+
export async function loadSettings(path = settingsPath()) {
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = await readFile(path, "utf8");
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === "ENOENT")
|
|
35
|
+
return {};
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return typeof parsed.model === "string" && parsed.model.trim() ? { model: parsed.model.trim() } : {};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A damaged file only loses the saved model; the next save rewrites it.
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function saveModel(model, path = settingsPath()) {
|
|
48
|
+
const settings = { ...(await loadSettings(path)), model };
|
|
49
|
+
await mkdir(dirname(path), { recursive: true });
|
|
50
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
51
|
+
await writeFile(temp, `${JSON.stringify({ version: 1, ...settings }, null, 2)}\n`, { mode: 0o600 });
|
|
52
|
+
await rename(temp, path);
|
|
53
|
+
}
|
|
54
|
+
/** The model to use when none is named: dl's setting, else Autopilot's default, with where it came from. */
|
|
55
|
+
export async function preferredModel(autopilotModel, path) {
|
|
56
|
+
const { model } = await loadSettings(path);
|
|
57
|
+
if (model)
|
|
58
|
+
return { model, source: "dl" };
|
|
59
|
+
const fromAutopilot = await autopilotModel();
|
|
60
|
+
return fromAutopilot ? { model: fromAutopilot, source: "autopilot" } : undefined;
|
|
61
|
+
}
|
|
62
|
+
/** This laptop's default model: the one set with `dl model`, else Autopilot's. */
|
|
63
|
+
export async function laptopModel() {
|
|
64
|
+
return (await preferredModel(() => readAutopilotModel()))?.model;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The laptop's Autopilot default model: KIMI_MODEL (Autopilot's own override) or the
|
|
68
|
+
* `model` in its config. undefined when neither is set or the config can't be read.
|
|
69
|
+
*/
|
|
70
|
+
export async function readAutopilotModel(sources = {}) {
|
|
71
|
+
const env = sources.env ?? process.env;
|
|
72
|
+
const fromEnv = env.KIMI_MODEL?.trim();
|
|
73
|
+
if (fromEnv)
|
|
74
|
+
return fromEnv;
|
|
75
|
+
const read = sources.readFile ?? ((path) => readFile(path, "utf8").catch(() => undefined));
|
|
76
|
+
const raw = await read(autopilotConfigPath(env, sources.home ?? homedir()));
|
|
77
|
+
if (!raw)
|
|
78
|
+
return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const model = JSON.parse(raw).model;
|
|
81
|
+
return typeof model === "string" && model.trim() ? model.trim() : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** OpenRouter's prices, or undefined when they can't be fetched: the list then just has no prices. */
|
|
88
|
+
export async function modelPrices(fetchImpl = fetch) {
|
|
89
|
+
return fetchModelPrices(fetchImpl).then(({ table }) => table, () => undefined);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The picker's options: the suggestions (with prices when known, and the current model
|
|
93
|
+
* added if it isn't one), then "Another model", which takes a typed ID.
|
|
94
|
+
*/
|
|
95
|
+
export function modelOptions(current, prices, anotherPrompt) {
|
|
96
|
+
const known = prices ? SUGGESTED_MODELS.filter((entry) => entry.id.toLowerCase() in prices) : SUGGESTED_MODELS;
|
|
97
|
+
const entries = current && !known.some((entry) => entry.id === current) ? [{ id: current, note: "your current model" }, ...known] : known;
|
|
98
|
+
return [
|
|
99
|
+
...entries.map((entry) => ({ value: entry.id, label: entry.id, detail: [entry.note, priceLabel(prices?.[entry.id.toLowerCase()])].filter(Boolean).join(" · ") })),
|
|
100
|
+
{ value: "another", label: "Another model", detail: "any OpenRouter model ID", input: { prompt: anotherPrompt, placeholder: OPENROUTER_ID_EXAMPLE } },
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function priceLabel(price) {
|
|
104
|
+
if (!price)
|
|
105
|
+
return "";
|
|
106
|
+
if (price.inputPerMTok === 0 && price.outputPerMTok === 0)
|
|
107
|
+
return "free";
|
|
108
|
+
return `$${price.inputPerMTok} in / $${price.outputPerMTok} out per M tokens`;
|
|
109
|
+
}
|
|
110
|
+
/** A typed model ID, or why it isn't one. Checked against OpenRouter's catalog when it's known. */
|
|
111
|
+
export function checkModelId(text, prices) {
|
|
112
|
+
const model = text.trim();
|
|
113
|
+
if (!/^[a-z0-9][\w.-]*\/[\w.:-]+$/i.test(model))
|
|
114
|
+
return { ok: false, message: `"${model}" isn't an OpenRouter model ID (${OPENROUTER_ID_EXAMPLE})` };
|
|
115
|
+
if (prices && !(model.toLowerCase() in prices))
|
|
116
|
+
return { ok: false, message: `OpenRouter has no model ${model} (see https://openrouter.ai/models)` };
|
|
117
|
+
return { ok: true, model };
|
|
118
|
+
}
|
|
119
|
+
/** The arrow-key list of models, with `current` preselected; a typed ID gets 3 tries. */
|
|
120
|
+
export async function askModel(prompter, ui, current, prices) {
|
|
121
|
+
const options = modelOptions(current, prices, ui.prompt("Model ID", OPENROUTER_ID_EXAMPLE));
|
|
122
|
+
const initial = Math.max(0, options.findIndex((option) => option.value === current));
|
|
123
|
+
for (let attempt = 1;; attempt++) {
|
|
124
|
+
const picked = await prompter.select(ui.prompt("Model", "used for tasks and sessions"), options, initial);
|
|
125
|
+
if (picked.value !== "another")
|
|
126
|
+
return picked.value;
|
|
127
|
+
const checked = checkModelId(picked.text ?? "", prices);
|
|
128
|
+
if (checked.ok)
|
|
129
|
+
return checked.model;
|
|
130
|
+
if (attempt >= 3)
|
|
131
|
+
throw new Error(checked.message);
|
|
132
|
+
ui.rejected(`${checked.message}. Try again.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `dl model [ID]`: the model `dl run` and new sessions use when -m doesn't name one. With an
|
|
137
|
+
* ID, sets it; without, picks from a list on a terminal, or says which it is otherwise.
|
|
138
|
+
*/
|
|
139
|
+
export async function modelCommand(args, deps) {
|
|
140
|
+
const c = palette(deps.color);
|
|
141
|
+
const current = await preferredModel(deps.autopilotModel, deps.settingsFile);
|
|
142
|
+
const model = current?.model ?? DEFAULT_MODEL;
|
|
143
|
+
const from = current?.source === "dl" ? "set with dl model" : current?.source === "autopilot" ? "Autopilot's default" : "dl's default";
|
|
144
|
+
if (args.length > 1) {
|
|
145
|
+
deps.write(`\n ${c.red("✗")} dl model takes one model ID, e.g. dl model ${DEFAULT_MODEL}\n\n`);
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
if (args.length === 0 && !deps.interactive) {
|
|
149
|
+
deps.write(`\n ${c.bold("Model")} ${model} ${c.dim(`(${from})`)}\n ${c.dim("Change it:")} dl model <id> ${c.dim("(model IDs: https://openrouter.ai/models)")}\n\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const prices = await modelPrices(deps.fetch);
|
|
153
|
+
let chosen;
|
|
154
|
+
if (args[0] !== undefined) {
|
|
155
|
+
const checked = checkModelId(args[0], prices);
|
|
156
|
+
if (!checked.ok) {
|
|
157
|
+
deps.write(`\n ${c.red("✗")} ${checked.message}\n\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
chosen = checked.model;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const ui = deps.newUi();
|
|
164
|
+
deps.write("\n");
|
|
165
|
+
try {
|
|
166
|
+
chosen = await askModel(deps.prompter, ui, model, prices);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (!(error instanceof PromptCancelled))
|
|
170
|
+
throw error;
|
|
171
|
+
deps.write(` ${c.dim(`Unchanged: ${model}`)}\n\n`);
|
|
172
|
+
return 130;
|
|
173
|
+
}
|
|
174
|
+
ui.finish();
|
|
175
|
+
}
|
|
176
|
+
await saveModel(chosen, deps.settingsFile);
|
|
177
|
+
deps.write(`\n ${c.green("✓")} ${c.bold("Model")} ${chosen}\n ${c.dim("Used by dl run and new sessions. A running session keeps its model; switch it there with")} /model\n\n`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/** `dl model` with the real terminal and network. */
|
|
181
|
+
export async function model(args, options) {
|
|
182
|
+
const { io, terminal } = options;
|
|
183
|
+
const color = io.color === true;
|
|
184
|
+
return modelCommand(args, {
|
|
185
|
+
newUi: () => terminal ? new TerminalUi(io.out, undefined, color) : new PlainUi(io.out),
|
|
186
|
+
prompter: new TtyPrompter({ color }),
|
|
187
|
+
write: io.out,
|
|
188
|
+
color,
|
|
189
|
+
interactive: terminal,
|
|
190
|
+
fetch: globalThis.fetch,
|
|
191
|
+
autopilotModel: () => readAutopilotModel(),
|
|
192
|
+
});
|
|
193
|
+
}
|
package/dist/laptop/setup.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loadRemotes, loadRemotesFile, normalizeServerUrl, REMOTE_NAME_RE, remot
|
|
|
9
9
|
import { confirm, gatherKeys } from "./credentials.js";
|
|
10
10
|
import { bootstrap, INSTALL_URL, installOnServer, pairLaptop, serverNeeds } from "./remote-install.js";
|
|
11
11
|
import { checkServer, describeHost, discoverKeys, hostOf, parseSshConfig, parseTarget, realSshExec, SshAuthError } from "./ssh.js";
|
|
12
|
+
import { askModel, DEFAULT_MODEL, loadSettings, modelPrices, readAutopilotModel, saveModel } from "./settings.js";
|
|
12
13
|
import { openRemote, withConnections } from "./tunnel.js";
|
|
13
14
|
const RUN_EXAMPLE = 'dl run "fix the flaky checkout test"';
|
|
14
15
|
const PLACES = [
|
|
@@ -40,6 +41,7 @@ export async function setupWizard(deps) {
|
|
|
40
41
|
await saveRemote(remote, remotesFile);
|
|
41
42
|
if (isNew)
|
|
42
43
|
await chooseDefault(deps, remote, remotesFile);
|
|
44
|
+
await chooseModel(deps);
|
|
43
45
|
finishScreen(deps.newUi(), capacity);
|
|
44
46
|
return remote;
|
|
45
47
|
}
|
|
@@ -100,6 +102,7 @@ async function setUpServer(deps) {
|
|
|
100
102
|
connectedUi.finish();
|
|
101
103
|
if (!known.some((entry) => entry.name === paired.name))
|
|
102
104
|
await chooseDefault(deps, remote, remotesFile);
|
|
105
|
+
await chooseModel(deps);
|
|
103
106
|
finishScreen(deps.newUi(), capacity);
|
|
104
107
|
return remote;
|
|
105
108
|
}
|
|
@@ -155,6 +158,21 @@ async function chooseDefault(deps, added, remotesFile) {
|
|
|
155
158
|
ui.answered("Default", value);
|
|
156
159
|
ui.finish();
|
|
157
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Which model tasks and sessions use, asked once: setup skips it when dl already has one
|
|
163
|
+
* (dl model changes it). Autopilot's own default is preselected for people who have one.
|
|
164
|
+
*/
|
|
165
|
+
async function chooseModel(deps) {
|
|
166
|
+
if ((await loadSettings(deps.settingsFile)).model)
|
|
167
|
+
return;
|
|
168
|
+
const [current, prices] = await Promise.all([readAutopilotModel(deps.sources), deps.sources.fetch ? modelPrices(deps.sources.fetch) : undefined]);
|
|
169
|
+
const ui = deps.newUi();
|
|
170
|
+
deps.write("\n");
|
|
171
|
+
const model = await askModel(deps.prompter, ui, current ?? DEFAULT_MODEL, prices);
|
|
172
|
+
await saveModel(model, deps.settingsFile);
|
|
173
|
+
ui.answered("Model", `${model} (change it any time: dl model)`);
|
|
174
|
+
ui.finish();
|
|
175
|
+
}
|
|
158
176
|
function describePlace(remote) {
|
|
159
177
|
if (remote.ssh)
|
|
160
178
|
return `server · ${remote.ssh.target}${remote.ssh.port ? `:${remote.ssh.port}` : ""}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directionallylocal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Directionally Local: background coding agents on your own server. One-line server install, and the dl command for your server and laptop.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://directionallylocal.com",
|