nolo-cli 0.1.29 → 0.1.30

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.
@@ -3,6 +3,7 @@ import { createSpaceKey } from "./create/space/spaceKeys";
3
3
  import { readDbRecord, writeDbRecord } from "./agentRecordHelpers";
4
4
  import {
5
5
  parseUserIdFromAuthToken,
6
+ readCliOptionValue,
6
7
  resolveAuthToken,
7
8
  resolveServerUrl,
8
9
  type EnvLike,
@@ -68,11 +69,13 @@ function buildAgentKeys(agentId: string, userId: string) {
68
69
 
69
70
  async function createCustomAgentWithCliAuth(
70
71
  options: CreateCustomCodingAgentOptions,
71
- deps: Pick<AgentCreateCustomDeps, "env" | "fallbackFetchImpl" | "fetchImpl">
72
+ deps: Pick<AgentCreateCustomDeps, "env" | "fallbackFetchImpl" | "fetchImpl"> & {
73
+ cliArgs?: string[];
74
+ }
72
75
  ) {
73
76
  const env = deps.env ?? process.env;
74
77
  const fetchImpl = deps.fetchImpl ?? fetch;
75
- const authToken = resolveAuthToken(env);
78
+ const authToken = readCliOptionValue(deps.cliArgs ?? [], "--token") || resolveAuthToken(env);
76
79
  const userId = parseUserIdFromAuthToken(authToken);
77
80
  if (!authToken || !userId) {
78
81
  throw new Error("agent create-custom requires an auth token with userId. Run `nolo login`.");
@@ -232,9 +235,11 @@ export async function runAgentCreateCustomCommand(
232
235
  }
233
236
 
234
237
  try {
235
- const createCustomAgent = deps.createCustomAgent ?? ((options) => createCustomAgentWithCliAuth(options, deps));
238
+ const createCustomAgent =
239
+ deps.createCustomAgent ??
240
+ ((options) => createCustomAgentWithCliAuth(options, { ...deps, cliArgs: args }));
236
241
  const result = await createCustomAgent({
237
- baseUrl: resolveServerUrl(env),
242
+ baseUrl: resolveServerUrl(args, env),
238
243
  spaceId: readOption(args, "--space-id"),
239
244
  agentId: env.CUSTOM_AGENT_ID,
240
245
  apiKey,
@@ -93,7 +93,7 @@ export async function runAgentDialogsCommand(
93
93
  return agentInput ? 0 : 1;
94
94
  }
95
95
 
96
- const authToken = resolveAuthToken(env);
96
+ const authToken = resolveAuthToken(args, env);
97
97
  if (!authToken) {
98
98
  output.write("[nolo] agent dialogs requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
99
99
  return 1;
@@ -112,12 +112,13 @@ export async function runAgentDialogsCommand(
112
112
  const db = deps.db ?? await getReadableCliDb(output);
113
113
  const fetchImpl = deps.fetchImpl ?? fetch;
114
114
  const fallbackFetchImpl = deps.fallbackFetchImpl;
115
- const serverUrl = resolveServerUrl(env);
116
- const serverCandidates = resolveServerCandidates(env, serverUrl);
115
+ const serverUrl = resolveServerUrl(args, env);
116
+ const serverCandidates = resolveServerCandidates(args, env, serverUrl);
117
117
 
118
118
  try {
119
119
  const resolvedAgent = await resolveAgentRecordFromHybridStore({
120
120
  agentInput,
121
+ cliArgs: args,
121
122
  env,
122
123
  db,
123
124
  fetchImpl,
@@ -45,7 +45,7 @@ export async function runAgentDoctorCommand(
45
45
  return 0;
46
46
  }
47
47
 
48
- const authToken = resolveAuthToken(env);
48
+ const authToken = resolveAuthToken(args, env);
49
49
  if (!authToken) {
50
50
  output.write("[nolo] agent doctor requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
51
51
  return 1;
@@ -60,8 +60,8 @@ export async function runAgentDoctorCommand(
60
60
  const db = deps.db ?? await getReadableCliDb(output);
61
61
  const fetchImpl = deps.fetchImpl ?? fetch;
62
62
  const fallbackFetchImpl = deps.fallbackFetchImpl;
63
- const defaultServerUrl = resolveServerUrl(env);
64
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
63
+ const defaultServerUrl = resolveServerUrl(args, env);
64
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
65
65
 
66
66
  try {
67
67
  const [profile, settings, agents] = await Promise.all([
@@ -25,7 +25,7 @@ export async function runAgentListCommand(
25
25
  const output = deps.output ?? process.stdout;
26
26
  const { wantJson, publicOnly, idsOnly, includeLegacy } = parseAgentListArgs(args);
27
27
 
28
- const authToken = resolveAuthToken(env);
28
+ const authToken = resolveAuthToken(args, env);
29
29
  if (!authToken) {
30
30
  output.write("[nolo] agent list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
31
31
  return 1;
@@ -40,8 +40,8 @@ export async function runAgentListCommand(
40
40
  const db = deps.db ?? await getReadableCliDb(output);
41
41
  const fetchImpl = deps.fetchImpl ?? fetch;
42
42
  const fallbackFetchImpl = deps.fallbackFetchImpl;
43
- const serverUrl = resolveServerUrl(env);
44
- const serverCandidates = resolveServerCandidates(env, serverUrl);
43
+ const serverUrl = resolveServerUrl(args, env);
44
+ const serverCandidates = resolveServerCandidates(args, env, serverUrl);
45
45
 
46
46
  try {
47
47
  const loadRemoteAgents = async () => {
@@ -225,7 +225,7 @@ export async function runAgentBindCurrentCommand(
225
225
  return agentKey ? 0 : 1;
226
226
  }
227
227
 
228
- const authToken = resolveAuthToken(env);
228
+ const authToken = resolveAuthToken(args, env);
229
229
  if (!authToken) {
230
230
  output.write("[nolo] agent bind-current requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
231
231
  return 1;
@@ -237,7 +237,7 @@ export async function runAgentBindCurrentCommand(
237
237
  return 1;
238
238
  }
239
239
 
240
- const serverUrl = resolveServerUrl(env);
240
+ const serverUrl = resolveServerUrl(args, env);
241
241
  const fetchImpl = deps.fetchImpl ?? fetch;
242
242
  const fallbackFetchImpl = deps.fallbackFetchImpl;
243
243
  const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
@@ -292,7 +292,7 @@ export async function runAgentSmokeCurrentCommand(
292
292
  }
293
293
  const agentKey = resolveCliAgentKeyInput(agentInput);
294
294
 
295
- const authToken = resolveAuthToken(env);
295
+ const authToken = resolveAuthToken(args, env);
296
296
  if (!authToken) {
297
297
  output.write("[nolo] agent smoke-current requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
298
298
  return 1;
@@ -304,7 +304,7 @@ export async function runAgentSmokeCurrentCommand(
304
304
  }
305
305
 
306
306
  const userInput = readOption(args, "--msg") ?? "Smoke test from nolo connector.";
307
- const serverUrl = resolveServerUrl(env);
307
+ const serverUrl = resolveServerUrl(args, env);
308
308
  const fetchImpl = deps.fetchImpl ?? fetch;
309
309
  const fallbackFetchImpl = deps.fallbackFetchImpl;
310
310
  const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
@@ -415,13 +415,13 @@ export async function runAgentRuntimeDoctorCommand(
415
415
  }
416
416
  const agentKey = resolveCliAgentKeyInput(agentInput);
417
417
 
418
- const authToken = resolveAuthToken(env);
418
+ const authToken = resolveAuthToken(args, env);
419
419
  if (!authToken) {
420
420
  output.write("[nolo] agent runtime-doctor requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
421
421
  return 1;
422
422
  }
423
423
 
424
- const serverUrl = resolveServerUrl(env);
424
+ const serverUrl = resolveServerUrl(args, env);
425
425
  const fetchImpl = deps.fetchImpl ?? fetch;
426
426
  const fallbackFetchImpl = deps.fallbackFetchImpl;
427
427
  const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
@@ -4,8 +4,8 @@ import type { OutputLike } from "./cliIoTypes";
4
4
  import { resolveCliAgentKeyInput } from "./agentAliases";
5
5
  import {
6
6
  readCliOptionValue,
7
- resolveAuthTokenFromEnv,
8
- resolveServerUrlFromEnv,
7
+ resolveAuthToken,
8
+ resolveServerUrl,
9
9
  } from "./cliEnvHelpers";
10
10
 
11
11
  type EnvLike = Record<string, string | undefined>;
@@ -44,14 +44,6 @@ export function parseAgentPullArgs(args: string[]): ParsedAgentPullArgs | null {
44
44
  return { agentKey: resolveCliAgentKeyInput(agentKey) };
45
45
  }
46
46
 
47
- function resolveServerUrl(env: EnvLike) {
48
- return resolveServerUrlFromEnv(env);
49
- }
50
-
51
- function resolveAuthToken(env: EnvLike) {
52
- return resolveAuthTokenFromEnv(env);
53
- }
54
-
55
47
  function extractToolNames(record: any) {
56
48
  if (Array.isArray(record?.toolNames)) return record.toolNames.filter((tool: unknown) => typeof tool === "string");
57
49
  if (!Array.isArray(record?.tools)) return [];
@@ -113,8 +105,8 @@ export async function runAgentPullCommand(args: string[], deps: AgentPullCommand
113
105
  return 1;
114
106
  }
115
107
 
116
- const serverUrl = resolveServerUrl(env);
117
- const authToken = resolveAuthToken(env);
108
+ const serverUrl = resolveServerUrl(args, env);
109
+ const authToken = resolveAuthToken(args, env);
118
110
  const fetchImpl = deps.fetchImpl ?? fetch;
119
111
  const db = deps.db ?? await getDefaultCliLocalRuntimeDb();
120
112
 
@@ -21,7 +21,7 @@ export async function runAgentReadCommand(
21
21
  return agentInput ? 0 : 1;
22
22
  }
23
23
 
24
- const authToken = resolveAuthToken(env);
24
+ const authToken = resolveAuthToken(args, env);
25
25
  if (!authToken) {
26
26
  output.write("[nolo] agent read requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
27
27
  return 1;
@@ -35,6 +35,7 @@ export async function runAgentReadCommand(
35
35
  try {
36
36
  const result = await resolveAgentRecordFromHybridStore({
37
37
  agentInput,
38
+ cliArgs: args,
38
39
  env,
39
40
  db,
40
41
  fetchImpl,
@@ -79,7 +80,7 @@ export async function runAgentUpdateCommand(
79
80
  return args[0] ? 0 : 1;
80
81
  }
81
82
 
82
- const authToken = resolveAuthToken(env);
83
+ const authToken = resolveAuthToken(args, env);
83
84
  if (!authToken) {
84
85
  output.write("[nolo] agent update requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
85
86
  return 1;
@@ -97,6 +98,7 @@ export async function runAgentUpdateCommand(
97
98
 
98
99
  try {
99
100
  const built = await buildUpdatedAgentRecord({
101
+ cliArgs: args,
100
102
  parsed,
101
103
  env,
102
104
  db,
@@ -262,14 +262,15 @@ export async function deleteDbRecord(args: {
262
262
 
263
263
  export async function resolveAgentRecordFromHybridStore(args: {
264
264
  agentInput: string;
265
+ cliArgs?: string[];
265
266
  env: EnvLike;
266
267
  db: CliKvDb;
267
268
  fetchImpl: typeof fetch;
268
269
  fallbackFetchImpl?: typeof fetch;
269
270
  }) {
270
271
  const agentKey = resolveCliAgentKeyInput(args.agentInput);
271
- const authToken = resolveAuthToken(args.env);
272
- const defaultServerUrl = resolveServerUrl(args.env);
272
+ const authToken = resolveAuthToken(args.cliArgs ?? [], args.env);
273
+ const defaultServerUrl = resolveServerUrl(args.cliArgs ?? [], args.env);
273
274
 
274
275
  for (const key of buildLocalAgentLookupKeys({
275
276
  agentRef: agentKey,
@@ -289,7 +290,7 @@ export async function resolveAgentRecordFromHybridStore(args: {
289
290
  }
290
291
 
291
292
  if (!shouldReadAgentKeyRemotely(key)) continue;
292
- for (const serverUrl of resolveServerCandidates(args.env, defaultServerUrl)) {
293
+ for (const serverUrl of resolveServerCandidates(args.cliArgs ?? [], args.env, defaultServerUrl)) {
293
294
  try {
294
295
  const remoteRecord = await readAgentRecord({
295
296
  agentKey: key,
@@ -380,6 +381,7 @@ export function parseAgentUpdateArgs(args: string[]) {
380
381
  }
381
382
 
382
383
  export async function buildUpdatedAgentRecord(args: {
384
+ cliArgs?: string[];
383
385
  parsed: NonNullable<ReturnType<typeof parseAgentUpdateArgs>>;
384
386
  env: EnvLike;
385
387
  db: CliKvDb;
@@ -389,13 +391,14 @@ export async function buildUpdatedAgentRecord(args: {
389
391
  }) {
390
392
  const cached = await resolveAgentRecordFromHybridStore({
391
393
  agentInput: args.parsed.agentInput,
394
+ cliArgs: args.cliArgs,
392
395
  env: args.env,
393
396
  db: args.db,
394
397
  fetchImpl: args.fetchImpl,
395
398
  fallbackFetchImpl: args.fallbackFetchImpl,
396
399
  });
397
400
  const agentKey = cached?.agentKey ?? resolveCliAgentKeyInput(args.parsed.agentInput);
398
- const serverUrl = cached?.record?.serverOrigin ?? resolveServerUrl(args.env);
401
+ const serverUrl = cached?.record?.serverOrigin ?? resolveServerUrl(args.cliArgs ?? [], args.env);
399
402
  const currentRecord = await readAgentRecord({
400
403
  agentKey,
401
404
  authToken: args.authToken,
@@ -407,6 +410,7 @@ export async function buildUpdatedAgentRecord(args: {
407
410
  if (args.parsed.copyProviderFrom) {
408
411
  const providerSource = await resolveAgentRecordFromHybridStore({
409
412
  agentInput: args.parsed.copyProviderFrom,
413
+ cliArgs: args.cliArgs,
410
414
  env: args.env,
411
415
  db: args.db,
412
416
  fetchImpl: args.fetchImpl,
@@ -1,4 +1,3 @@
1
- import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
1
  import { runAgentTurn, type RunAgentTurnResult } from "./client/agentRun";
3
2
  import type { AgentRuntimeHostAdapter } from "./agentRuntimeLocal";
4
3
  import {
@@ -20,6 +19,7 @@ import {
20
19
  type ParsedAgentRunArgs,
21
20
  type ResolvedWorkflowReference,
22
21
  } from "./agentRunParsing";
22
+ import { resolveServerUrl } from "./cliEnvHelpers";
23
23
 
24
24
  type AgentRunCommandDeps = {
25
25
  env?: EnvLike;
@@ -49,10 +49,6 @@ function shouldPrintLocalRunSummary(args: {
49
49
  return Boolean(args.localRuntimeCwd && args.parsed.runtimeMode === "local");
50
50
  }
51
51
 
52
- function resolveServerUrl(env: EnvLike) {
53
- return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
54
- }
55
-
56
52
  function writeUsage(output: OutputLike) {
57
53
  output.write(
58
54
  "Usage: nolo agent run <agent> <message> [--local|--server|--auto] [--continue <dialogId>] [--cwd <path>] [--max-tool-rounds <n>]\n" +
@@ -95,7 +91,7 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
95
91
  const result: RunAgentTurnResult = await runner({
96
92
  agentName: parsed.agentKey,
97
93
  agentKey: parsed.agentKey,
98
- serverUrl: resolveServerUrl(env),
94
+ serverUrl: resolveServerUrl(args, env),
99
95
  message: prependWorkflowReferencePrompt(parsed.message, workflowReference),
100
96
  imageUrls: parsed.imageUrls.map(normalizeCliImageInput),
101
97
  scriptDir: deps.scriptDir,
@@ -36,7 +36,7 @@ export async function runAgentUnpublishCommand(
36
36
  return 1;
37
37
  }
38
38
 
39
- const authToken = resolveAuthToken(env);
39
+ const authToken = resolveAuthToken(args, env);
40
40
  if (!authToken) {
41
41
  output.write("[nolo] agent unpublish requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
42
42
  return 1;
@@ -51,13 +51,14 @@ export async function runAgentUnpublishCommand(
51
51
  const db = deps.db ?? await getReadableCliDb(output);
52
52
  const fetchImpl = deps.fetchImpl ?? fetch;
53
53
  const fallbackFetchImpl = deps.fallbackFetchImpl;
54
- const serverUrl = resolveServerUrl(env);
54
+ const serverUrl = resolveServerUrl(args, env);
55
55
  const privateKey = `agent-${userId}-${agentId}`;
56
56
  const publicKey = `agent-pub-${agentId}`;
57
57
 
58
58
  try {
59
59
  const resolved = await resolveAgentRecordFromHybridStore({
60
60
  agentInput: privateKey,
61
+ cliArgs: args,
61
62
  env,
62
63
  db,
63
64
  fetchImpl,
package/cliEnvHelpers.ts CHANGED
@@ -49,19 +49,13 @@ export function resolveServerUrlFromArgs(args: string[], env: EnvLike) {
49
49
  );
50
50
  }
51
51
 
52
- export function resolveServerUrl(env: EnvLike) {
53
- return resolveServerUrlFromEnv(env);
52
+ type ServerResolverInput = string[] | EnvLike;
53
+
54
+ function isCliArgs(value: ServerResolverInput): value is string[] {
55
+ return Array.isArray(value);
54
56
  }
55
57
 
56
- export function resolveServerCandidates(env: EnvLike, preferred?: string | null) {
57
- const values = [
58
- preferred?.trim(),
59
- env.NOLO_SERVER,
60
- env.BASE_URL,
61
- env.NOLO_SERVER_URL,
62
- DEFAULT_NOLO_SERVER_URL,
63
- ...NOLO_CLUSTER_SERVERS,
64
- ];
58
+ function buildServerCandidates(values: Array<string | null | undefined>) {
65
59
  return [...new Set(
66
60
  values
67
61
  .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
@@ -69,8 +63,61 @@ export function resolveServerCandidates(env: EnvLike, preferred?: string | null)
69
63
  )];
70
64
  }
71
65
 
72
- export function resolveAuthToken(env: EnvLike) {
73
- return resolveAuthTokenFromEnv(env);
66
+ export function resolveServerUrl(env: EnvLike): string;
67
+ export function resolveServerUrl(args: string[], env: EnvLike): string;
68
+ export function resolveServerUrl(input: ServerResolverInput, env?: EnvLike) {
69
+ if (isCliArgs(input)) {
70
+ return resolveServerUrlFromArgs(input, env ?? {});
71
+ }
72
+ return resolveServerUrlFromEnv(input);
73
+ }
74
+
75
+ export function resolveServerCandidates(env: EnvLike, preferred?: string | null): string[];
76
+ export function resolveServerCandidates(
77
+ args: string[],
78
+ env: EnvLike,
79
+ preferred?: string | null,
80
+ ): string[];
81
+ export function resolveServerCandidates(
82
+ input: ServerResolverInput,
83
+ envOrPreferred?: EnvLike | string | null,
84
+ preferred?: string | null,
85
+ ) {
86
+ if (isCliArgs(input)) {
87
+ const env = (envOrPreferred ?? {}) as EnvLike;
88
+ const explicitServer = resolveServerUrlFromArgs(input, env);
89
+ return buildServerCandidates([
90
+ explicitServer,
91
+ preferred?.trim(),
92
+ env.NOLO_SERVER,
93
+ env.BASE_URL,
94
+ env.NOLO_SERVER_URL,
95
+ DEFAULT_NOLO_SERVER_URL,
96
+ ...NOLO_CLUSTER_SERVERS,
97
+ ]);
98
+ }
99
+ const env = input;
100
+ return buildServerCandidates([
101
+ typeof envOrPreferred === "string" ? envOrPreferred.trim() : undefined,
102
+ env.NOLO_SERVER,
103
+ env.BASE_URL,
104
+ env.NOLO_SERVER_URL,
105
+ DEFAULT_NOLO_SERVER_URL,
106
+ ...NOLO_CLUSTER_SERVERS,
107
+ ]);
108
+ }
109
+
110
+ export function resolveAuthToken(env: EnvLike, extraEnvKeys?: string[]): string;
111
+ export function resolveAuthToken(args: string[], env: EnvLike, extraEnvKeys?: string[]): string;
112
+ export function resolveAuthToken(
113
+ input: ServerResolverInput,
114
+ envOrExtraEnvKeys?: EnvLike | string[],
115
+ extraEnvKeys: string[] = [],
116
+ ) {
117
+ if (isCliArgs(input)) {
118
+ return resolveAuthTokenFromArgs(input, (envOrExtraEnvKeys ?? {}) as EnvLike, extraEnvKeys);
119
+ }
120
+ return resolveAuthTokenFromEnv(input, (envOrExtraEnvKeys as string[] | undefined) ?? []);
74
121
  }
75
122
 
76
123
  export function resolveAuthTokenFromEnv(env: EnvLike, extraEnvKeys: string[] = []) {
@@ -1,6 +1,20 @@
1
- export async function resolveConnectorWebSocketTarget(input: {
2
- serverUrl: string;
3
- machineId: string;
1
+ export class ConnectorWebSocketAuthError extends Error {
2
+ code: string;
3
+
4
+ constructor(message: string, code = "CONNECTOR_AUTH_FAILED") {
5
+ super(message);
6
+ this.name = "ConnectorWebSocketAuthError";
7
+ this.code = code;
8
+ }
9
+ }
10
+
11
+ export function isConnectorWebSocketAuthError(error: unknown): error is ConnectorWebSocketAuthError {
12
+ return error instanceof ConnectorWebSocketAuthError;
13
+ }
14
+
15
+ export async function resolveConnectorWebSocketTarget(input: {
16
+ serverUrl: string;
17
+ machineId: string;
4
18
  headers: Record<string, string>;
5
19
  fetchImpl?: typeof fetch;
6
20
  }) {
@@ -14,13 +28,20 @@ export async function resolveConnectorWebSocketTarget(input: {
14
28
  probeUrl.pathname = "/api/connector/ws";
15
29
  probeUrl.search = directWsUrl.search;
16
30
 
17
- const response = await (input.fetchImpl ?? fetch)(probeUrl, {
18
- headers: input.headers,
19
- }).catch(() => null);
20
-
21
- if (!response || !response.ok) {
22
- return directWsUrl.toString();
23
- }
31
+ const response = await (input.fetchImpl ?? fetch)(probeUrl, {
32
+ headers: input.headers,
33
+ }).catch(() => null);
34
+
35
+ if (response && (response.status === 401 || response.status === 403)) {
36
+ const json = await response.json().catch(() => null);
37
+ const code = typeof json?.code === "string" ? json.code : "CONNECTOR_AUTH_FAILED";
38
+ const message = typeof json?.error === "string" ? json.error : `Connector auth failed with HTTP ${response.status}`;
39
+ throw new ConnectorWebSocketAuthError(`${code}: ${message}`, code);
40
+ }
41
+
42
+ if (!response || !response.ok) {
43
+ return directWsUrl.toString();
44
+ }
24
45
 
25
46
  const json = await response.json().catch(() => null);
26
47
  return typeof json?.wsUrl === "string" && json.wsUrl
@@ -60,7 +60,7 @@ export async function runDialogReadCommand(
60
60
  return args[0] ? 0 : 1;
61
61
  }
62
62
 
63
- const authToken = resolveAuthToken(env);
63
+ const authToken = resolveAuthToken(args, env);
64
64
  const fetchImpl = deps.fetchImpl ?? fetch;
65
65
  const fallbackFetchImpl = deps.fallbackFetchImpl;
66
66
 
@@ -68,6 +68,7 @@ export async function runDialogReadCommand(
68
68
  const { base, dialogId, userId, spaceId } = parseDialogInputForCli(parsedArgs.rawInput, env);
69
69
  const dialogKey = `dialog-${userId}-${dialogId}`;
70
70
  const candidateBases = resolveServerCandidates(
71
+ args,
71
72
  {
72
73
  ...env,
73
74
  BASE_URL: env.READ_DIALOG_BASE?.trim() || base || env.BASE_URL,
@@ -31,7 +31,7 @@ export async function runDialogStatusCommand(
31
31
  return rawInput ? 0 : 1;
32
32
  }
33
33
 
34
- const authToken = resolveAuthToken(env);
34
+ const authToken = resolveAuthToken(args, env);
35
35
  const fetchImpl = deps.fetchImpl ?? fetch;
36
36
  const fallbackFetchImpl = deps.fallbackFetchImpl;
37
37
 
@@ -39,6 +39,7 @@ export async function runDialogStatusCommand(
39
39
  const { base, dialogId, userId } = parseDialogInputForCli(rawInput, env);
40
40
  const dialogKey = `dialog-${userId}-${dialogId}`;
41
41
  const candidateBases = resolveServerCandidates(
42
+ args,
42
43
  {
43
44
  ...env,
44
45
  BASE_URL: env.READ_DIALOG_BASE?.trim() || base || env.BASE_URL,
@@ -161,6 +161,7 @@ export async function runMachineConnectCommand(
161
161
  }
162
162
  ),
163
163
  });
164
+ if (exitCode === 2) return 1;
164
165
  if (attempt >= maxAttempts) return exitCode;
165
166
  output.write(`[nolo] Connector websocket disconnected. Reconnecting in ${reconnectDelayMs}ms.\n`);
166
167
  await sleep(reconnectDelayMs);
@@ -1,5 +1,5 @@
1
- import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
1
  import type { OutputLike } from "./cliIoTypes";
2
+ import { resolveAuthToken, resolveServerUrl } from "./cliEnvHelpers";
3
3
 
4
4
  type EnvLike = Record<string, string | undefined>;
5
5
 
@@ -21,14 +21,6 @@ export type MachineStatusCommandDeps = {
21
21
  fetchImpl?: typeof fetch;
22
22
  };
23
23
 
24
- function resolveServerUrl(env: EnvLike) {
25
- return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
26
- }
27
-
28
- function resolveAuthToken(env: EnvLike) {
29
- return env.AUTH_TOKEN || env.AUTH || "";
30
- }
31
-
32
24
  function writeAuthMissing(output: OutputLike) {
33
25
  output.write(
34
26
  "[nolo] Machine commands require an auth token. Run `nolo login` or set AUTH_TOKEN.\n"
@@ -52,18 +44,18 @@ export function formatMachineStatus(machines: MachineSummary[]) {
52
44
  }
53
45
 
54
46
  export async function runMachineStatusCommand(
55
- _args: string[],
47
+ args: string[],
56
48
  deps: MachineStatusCommandDeps = {}
57
49
  ) {
58
50
  const env = deps.env ?? process.env;
59
51
  const output = deps.output ?? process.stdout;
60
- const authToken = resolveAuthToken(env);
52
+ const authToken = resolveAuthToken(args, env);
61
53
  if (!authToken) {
62
54
  writeAuthMissing(output);
63
55
  return 1;
64
56
  }
65
57
 
66
- const serverUrl = resolveServerUrl(env);
58
+ const serverUrl = resolveServerUrl(args, env);
67
59
  const fetchImpl = deps.fetchImpl ?? fetch;
68
60
  let res: Response;
69
61
  try {
@@ -1,5 +1,6 @@
1
1
  import type { HeartbeatLoopOptions } from "./connector-experimental/heartbeatLoop";
2
2
  import type { MachineHeartbeat } from "./connector-experimental/protocol";
3
+ import { isConnectorWebSocketAuthError } from "./connectorWebSocketTarget";
3
4
 
4
5
  type EnvLike = Record<string, string | undefined>;
5
6
  type OutputLike = { write(chunk: string): unknown };
@@ -138,6 +139,12 @@ export async function runMachineWsSession(options: {
138
139
  await Promise.allSettled(
139
140
  [websocketPromise, heartbeatLoopPromise].filter(Boolean) as Promise<void>[],
140
141
  );
142
+ if (isConnectorWebSocketAuthError(error)) {
143
+ options.output.write(
144
+ `[nolo] Connector websocket auth failed: ${error.message}. Re-register this machine or update NOLO_MACHINE_API_KEY.\n`
145
+ );
146
+ return 2;
147
+ }
141
148
  options.output.write(
142
149
  `[nolo] Connector websocket failed: ${
143
150
  error instanceof Error ? error.message : String(error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {
@@ -110,10 +110,10 @@
110
110
  "ai/**/*.ts",
111
111
  "chat/**/*.ts",
112
112
  "render/**/*.ts",
113
- "core/**/*.ts",
114
113
  "auth/**/*.ts",
115
- "life/**/*.ts",
116
114
  "share/**/*.ts",
115
+ "life/**/*.ts",
116
+ "core/**/*.ts",
117
117
  "server/**/*.ts",
118
118
  "integrations/**/*.ts"
119
119
  ],
@@ -136,24 +136,23 @@
136
136
  "tweetnacl": "^1.0.3",
137
137
  "@reduxjs/toolkit": "^2.5.0",
138
138
  "react-redux": "^9.2.0",
139
- "react-hot-toast": "^2.4.1",
140
139
  "react": "^19.2.1",
141
- "browser-image-compression": "^2.0.2",
140
+ "react-hot-toast": "^2.4.1",
142
141
  "react-i18next": "^14.1.2",
143
142
  "i18next": "^23.11.5",
144
- "ulid": "^2.3.0",
143
+ "browser-image-compression": "^2.0.2",
145
144
  "pino": "^9.9.0",
145
+ "ulid": "^2.3.0",
146
146
  "slate": "^0.112.0",
147
+ "slate-react": "^0.112.1",
148
+ "pdfjs-dist": "^5.2.133",
149
+ "slate-history": "^0.113.1",
147
150
  "mdast-util-from-markdown": "^2.0.2",
148
151
  "mdast-util-gfm": "^3.0.0",
149
152
  "micromark-extension-gfm": "^3.0.0",
150
- "slate-history": "^0.113.1",
151
- "slate-react": "^0.112.1",
152
- "pdfjs-dist": "^5.2.133",
153
- "rambda": "^8.6.0",
154
- "react-native-blob-util": "^0.24.7",
155
153
  "@react-native-community/netinfo": "^11.5.0",
156
- "react-native-sse": "^1.2.1",
154
+ "react-native-blob-util": "^0.24.7",
155
+ "rambda": "^8.6.0",
157
156
  "@lobehub/icons": "^5.0.1",
158
157
  "pizzip": "^3.2.0",
159
158
  "docxtemplater": "^3.67.5",
@@ -161,16 +160,17 @@
161
160
  "zod": "^3.25.20",
162
161
  "react-hook-form": "^7.51.5",
163
162
  "@hookform/resolvers": "^3.3.2",
163
+ "react-native-sse": "^1.2.1",
164
+ "xlsx": "^0.18.5",
164
165
  "react-router-dom": "^6.23.1",
165
166
  "croner": "^8.0.1",
166
- "xlsx": "^0.18.5",
167
167
  "mermaid": "^11.6.0",
168
- "js-base64": "^3.7.7",
169
168
  "react-native-keychain": "^9.2.2",
170
169
  "async-mutex": "^0.5.0",
171
- "diff": "^8.0.2",
170
+ "js-base64": "^3.7.7",
172
171
  "react-dom": "^19.2.1",
173
- "@waffo/pancake-ts": "^0.5.2",
174
- "playwright": "^1.59.1"
172
+ "diff": "^8.0.2",
173
+ "playwright": "^1.59.1",
174
+ "@waffo/pancake-ts": "^0.5.2"
175
175
  }
176
176
  }
@@ -66,7 +66,7 @@ async function runBasePageCreateCommand(
66
66
  return 1;
67
67
  }
68
68
 
69
- const authToken = resolveAuthToken(env);
69
+ const authToken = resolveAuthToken(args, env);
70
70
  const userId = parseUserIdFromAuthToken(authToken);
71
71
  if (!authToken || !userId) {
72
72
  output.write(`[nolo] ${mode} create requires an auth token with userId. Run \`nolo login\`.\n`);
@@ -82,7 +82,7 @@ async function runBasePageCreateCommand(
82
82
  return 1;
83
83
  }
84
84
 
85
- const serverUrl = resolveServerUrl(env);
85
+ const serverUrl = resolveServerUrl(args, env);
86
86
  const db = deps.db ?? await getReadableCliDb(output);
87
87
  const fetchImpl = deps.fetchImpl ?? fetch;
88
88
  const fallbackFetchImpl = deps.fallbackFetchImpl;
@@ -54,7 +54,7 @@ export async function runBasePageDeleteCommand(
54
54
  return dbKey ? 0 : 1;
55
55
  }
56
56
 
57
- const authToken = resolveAuthToken(env);
57
+ const authToken = resolveAuthToken(args, env);
58
58
  const userId = parseUserIdFromAuthToken(authToken);
59
59
  if (!authToken || !userId) {
60
60
  output.write(`[nolo] ${mode} delete requires an auth token with userId. Run \`nolo login\`.\n`);
@@ -64,8 +64,8 @@ export async function runBasePageDeleteCommand(
64
64
  const db = deps.db ?? await getReadableCliDb(output);
65
65
  const fetchImpl = deps.fetchImpl ?? fetch;
66
66
  const fallbackFetchImpl = deps.fallbackFetchImpl;
67
- const defaultServerUrl = resolveServerUrl(env);
68
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
67
+ const defaultServerUrl = resolveServerUrl(args, env);
68
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
69
69
 
70
70
  try {
71
71
  const existing = await readRecord({
@@ -76,7 +76,7 @@ async function runBasePageReadCommand(
76
76
  return raw ? 0 : 1;
77
77
  }
78
78
 
79
- const authToken = resolveAuthToken(env);
79
+ const authToken = resolveAuthToken(args, env);
80
80
  if (!authToken) {
81
81
  output.write(`[nolo] ${mode} read requires an auth token. Run \`nolo login\` or set AUTH_TOKEN.\n`);
82
82
  return 1;
@@ -85,8 +85,8 @@ async function runBasePageReadCommand(
85
85
  const db = deps.db ?? await getReadableCliDb(output);
86
86
  const fetchImpl = deps.fetchImpl ?? fetch;
87
87
  const fallbackFetchImpl = deps.fallbackFetchImpl;
88
- const defaultServerUrl = resolveServerUrl(env);
89
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
88
+ const defaultServerUrl = resolveServerUrl(args, env);
89
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
90
90
 
91
91
  try {
92
92
  const record = await readPageRecord({
@@ -63,7 +63,7 @@ async function runBasePageUpdateCommand(
63
63
  return dbKey ? 0 : 1;
64
64
  }
65
65
 
66
- const authToken = resolveAuthToken(env);
66
+ const authToken = resolveAuthToken(args, env);
67
67
  const userId = parseUserIdFromAuthToken(authToken);
68
68
  if (!authToken || !userId) {
69
69
  output.write(`[nolo] ${mode} update requires an auth token with userId. Run \`nolo login\`.\n`);
@@ -73,8 +73,8 @@ async function runBasePageUpdateCommand(
73
73
  const db = deps.db ?? await getReadableCliDb(output);
74
74
  const fetchImpl = deps.fetchImpl ?? fetch;
75
75
  const fallbackFetchImpl = deps.fallbackFetchImpl;
76
- const defaultServerUrl = resolveServerUrl(env);
77
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
76
+ const defaultServerUrl = resolveServerUrl(args, env);
77
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
78
78
  const writeTargets = [defaultServerUrl];
79
79
 
80
80
  try {
@@ -64,13 +64,13 @@ async function defaultLocalRuntimeProbe(env: EnvLike): Promise<LocalRuntimeProbe
64
64
  }
65
65
 
66
66
  export async function runDoctorRuntimeCommand(
67
- _args: string[],
67
+ args: string[],
68
68
  deps: AgentCommandDeps = {}
69
69
  ) {
70
70
  const env = deps.env ?? process.env;
71
71
  const output = deps.output ?? process.stdout;
72
- const authToken = resolveAuthToken(env);
73
- const serverUrl = resolveServerUrl(env);
72
+ const authToken = resolveAuthToken(args, env);
73
+ const serverUrl = resolveServerUrl(args, env);
74
74
  const localProbe = await (deps.localRuntimeProbe ?? defaultLocalRuntimeProbe)(env);
75
75
  const hasLocalAgentConfig = detectLocalAgentConfig(env) && localProbe.ok && localProbe.agentFound;
76
76
  const hasLocalProvider = detectLocalProvider(env);
@@ -49,7 +49,7 @@ export async function runSpaceCategoryCommand(
49
49
  return 1;
50
50
  }
51
51
 
52
- const authToken = resolveAuthToken(env);
52
+ const authToken = resolveAuthToken(args, env);
53
53
  const userId = parseUserIdFromAuthToken(authToken);
54
54
  if (!authToken || !userId) {
55
55
  output.write("[nolo] space category requires an auth token with userId. Run `nolo login`.\n");
@@ -66,8 +66,8 @@ export async function runSpaceCategoryCommand(
66
66
  const db = deps.db ?? await getReadableCliDb(output);
67
67
  const fetchImpl = deps.fetchImpl ?? fetch;
68
68
  const fallbackFetchImpl = deps.fallbackFetchImpl;
69
- const defaultServerUrl = resolveServerUrl(env);
70
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
69
+ const defaultServerUrl = resolveServerUrl(args, env);
70
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
71
71
 
72
72
  try {
73
73
  const spaceId = parseSpaceIdFromInput(rawSpace);
@@ -163,7 +163,7 @@ export async function runSpaceContentCategoryCommand(
163
163
  return 1;
164
164
  }
165
165
 
166
- const authToken = resolveAuthToken(env);
166
+ const authToken = resolveAuthToken(args, env);
167
167
  const userId = parseUserIdFromAuthToken(authToken);
168
168
  if (!authToken || !userId) {
169
169
  output.write("[nolo] space content-category requires an auth token with userId. Run `nolo login`.\n");
@@ -189,8 +189,8 @@ export async function runSpaceContentCategoryCommand(
189
189
  const db = deps.db ?? await getReadableCliDb(output);
190
190
  const fetchImpl = deps.fetchImpl ?? fetch;
191
191
  const fallbackFetchImpl = deps.fallbackFetchImpl;
192
- const defaultServerUrl = resolveServerUrl(env);
193
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
192
+ const defaultServerUrl = resolveServerUrl(args, env);
193
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
194
194
 
195
195
  try {
196
196
  const spaceId = parseSpaceIdFromInput(rawSpace);
package/spaceCommands.ts CHANGED
@@ -181,7 +181,7 @@ export async function runSpaceListCommand(
181
181
  const output = deps.output ?? process.stdout;
182
182
  const filters = parseSpaceListArgs(args);
183
183
 
184
- const authToken = resolveAuthToken(env);
184
+ const authToken = resolveAuthToken(args, env);
185
185
  if (!authToken) {
186
186
  output.write("[nolo] space list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
187
187
  return 1;
@@ -196,8 +196,8 @@ export async function runSpaceListCommand(
196
196
  const db = deps.db ?? await getReadableCliDb(output);
197
197
  const fetchImpl = deps.fetchImpl ?? fetch;
198
198
  const fallbackFetchImpl = deps.fallbackFetchImpl;
199
- const defaultServerUrl = resolveServerUrl(env);
200
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
199
+ const defaultServerUrl = resolveServerUrl(args, env);
200
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
201
201
 
202
202
  try {
203
203
  let spaces: ListedSpace[];
@@ -195,7 +195,7 @@ export async function runSpaceDeleteCommand(
195
195
  return 1;
196
196
  }
197
197
 
198
- const authToken = resolveAuthToken(env);
198
+ const authToken = resolveAuthToken(args, env);
199
199
  const userId = parseUserIdFromAuthToken(authToken);
200
200
  if (!authToken || !userId) {
201
201
  output.write("[nolo] space delete requires an auth token with userId. Run `nolo login`.\n");
@@ -207,7 +207,7 @@ export async function runSpaceDeleteCommand(
207
207
  const db = deps.db ?? await getReadableCliDb(output);
208
208
  const fetchImpl = deps.fetchImpl ?? fetch;
209
209
  const fallbackFetchImpl = deps.fallbackFetchImpl;
210
- const serverUrl = resolveServerUrl(env);
210
+ const serverUrl = resolveServerUrl(args, env);
211
211
 
212
212
  try {
213
213
  const memberships = await fetchMemberships({
@@ -79,7 +79,7 @@ export async function runSpaceReadCommand(
79
79
  const output = deps.output ?? process.stdout;
80
80
  const { raw, brief, contentKeyFilter, includeRecord } = parseSpaceReadArgs(args);
81
81
 
82
- const authToken = resolveAuthToken(env);
82
+ const authToken = resolveAuthToken(args, env);
83
83
  if (!authToken) {
84
84
  output.write("[nolo] space read requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
85
85
  return 1;
@@ -94,8 +94,8 @@ export async function runSpaceReadCommand(
94
94
  const db = deps.db ?? await getReadableCliDb(output);
95
95
  const fetchImpl = deps.fetchImpl ?? fetch;
96
96
  const fallbackFetchImpl = deps.fallbackFetchImpl;
97
- const defaultServerUrl = resolveServerUrl(env);
98
- const serverCandidates = resolveServerCandidates(env, defaultServerUrl);
97
+ const defaultServerUrl = resolveServerUrl(args, env);
98
+ const serverCandidates = resolveServerCandidates(args, env, defaultServerUrl);
99
99
 
100
100
  try {
101
101
  const spaceId = parseSpaceIdFromInput(raw);
@@ -158,7 +158,7 @@ export async function runTableMetaCommand(
158
158
  return 1;
159
159
  }
160
160
 
161
- const authToken = resolveAuthToken(env);
161
+ const authToken = resolveAuthToken(args, env);
162
162
  const userId = parseUserIdFromAuthToken(authToken);
163
163
  if (!authToken || !userId) {
164
164
  output.write("[nolo] table meta requires an auth token with userId. Run `nolo login`.\n");
@@ -168,7 +168,7 @@ export async function runTableMetaCommand(
168
168
  const name = rawName.trim();
169
169
  const description = readOption(args, "--description")?.trim() || undefined;
170
170
  const tags = parseTags(readOption(args, "--tags"));
171
- const serverUrl = resolveServerUrl(env);
171
+ const serverUrl = resolveServerUrl(args, env);
172
172
  const db = deps.db ?? await getReadableCliDb(output);
173
173
  const fetchImpl = deps.fetchImpl ?? fetch;
174
174
  const fallbackFetchImpl = deps.fallbackFetchImpl;
package/taskRunCommand.ts CHANGED
@@ -13,6 +13,15 @@ type TaskRunCommandDeps = {
13
13
  fetchImpl?: typeof fetch;
14
14
  };
15
15
 
16
+ const DEFAULT_TASK_RUN_TIMEOUT_MS = 10 * 60 * 1000;
17
+
18
+ function resolveTaskRunTimeoutMs(env: EnvLike) {
19
+ const raw = env.NOLO_TASK_RUN_TIMEOUT_MS;
20
+ if (!raw) return DEFAULT_TASK_RUN_TIMEOUT_MS;
21
+ const value = Number(raw);
22
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_TASK_RUN_TIMEOUT_MS;
23
+ }
24
+
16
25
  export async function runTaskRunCommand(args: string[], deps: TaskRunCommandDeps = {}) {
17
26
  const env = loadTaskRunEnv(deps.env ?? process.env, { includeDefaultEnvFiles: deps.env === undefined });
18
27
  const output = deps.output ?? process.stdout;
@@ -38,6 +47,7 @@ export async function runTaskRunCommand(args: string[], deps: TaskRunCommandDeps
38
47
  }
39
48
 
40
49
  const serverUrl = resolveServerUrl(args, env);
50
+ const timeoutMs = resolveTaskRunTimeoutMs(env);
41
51
  let response: Response;
42
52
  try {
43
53
  response = await fetchImpl(`${serverUrl}/api/task-run`, {
@@ -47,6 +57,7 @@ export async function runTaskRunCommand(args: string[], deps: TaskRunCommandDeps
47
57
  "Content-Type": "application/json",
48
58
  },
49
59
  body: JSON.stringify(body),
60
+ signal: AbortSignal.timeout(timeoutMs),
50
61
  });
51
62
  } catch (error) {
52
63
  output.write(`[nolo] task-run failed: ${error instanceof Error ? error.message : String(error)}\n`);