@nurix/nustack 0.10.0-dev.8 → 0.11.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.
Files changed (74) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/bootstrap.js +4 -4
  3. package/dist/commands/docs.js +13 -13
  4. package/dist/commands/doctor.js +51 -17
  5. package/dist/commands/handoff.js +8 -8
  6. package/dist/commands/init.js +43 -43
  7. package/dist/commands/login.js +19 -6
  8. package/dist/commands/logout.js +14 -6
  9. package/dist/commands/logscope.js +55 -0
  10. package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
  11. package/dist/commands/{workspace.js → project.js} +133 -133
  12. package/dist/commands/review.js +38 -15
  13. package/dist/commands/search.js +7 -7
  14. package/dist/commands/sessions.js +156 -5
  15. package/dist/commands/sync.js +10 -10
  16. package/dist/commands/token.js +102 -0
  17. package/dist/commands/toolkit.js +19 -19
  18. package/dist/index.js +88 -36
  19. package/dist/lib/auth_grant.js +32 -0
  20. package/dist/lib/auth_recovery.js +3 -1
  21. package/dist/lib/credential_store.js +106 -0
  22. package/dist/lib/device_identity.js +6 -6
  23. package/dist/lib/device_keychain.js +16 -37
  24. package/dist/lib/etna/profile.js +9 -9
  25. package/dist/lib/etna/reconcile.js +11 -11
  26. package/dist/lib/git-sync/core/engine.js +4 -4
  27. package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
  28. package/dist/lib/git-sync/inbound/approval.js +1 -1
  29. package/dist/lib/git-sync/inbound/download.js +8 -8
  30. package/dist/lib/git-sync/inbound/event_sync.js +13 -13
  31. package/dist/lib/git-sync/inbound/notification.js +1 -1
  32. package/dist/lib/git-sync/outbound/extraction.js +4 -4
  33. package/dist/lib/git-sync/outbound/history_decision.js +2 -2
  34. package/dist/lib/git-sync/outbound/publisher.js +2 -2
  35. package/dist/lib/git-sync/outbound/queue.js +3 -3
  36. package/dist/lib/git-sync/outbound/upload_client.js +4 -4
  37. package/dist/lib/git-sync/whitelist/index.js +1 -1
  38. package/dist/lib/git-sync/whitelist/types.js +1 -1
  39. package/dist/lib/git-sync/whitelist/validation.js +5 -5
  40. package/dist/lib/libsecret_store.js +47 -0
  41. package/dist/lib/local_store/accepted_uploads.js +5 -5
  42. package/dist/lib/local_store/bindings.js +11 -11
  43. package/dist/lib/local_store/index.js +1 -1
  44. package/dist/lib/local_store/migrations.js +13 -0
  45. package/dist/lib/local_store/project_events_cache.js +54 -0
  46. package/dist/lib/logs/answer.js +99 -0
  47. package/dist/lib/logs/library.js +19 -0
  48. package/dist/lib/logs/lifecycle.js +159 -0
  49. package/dist/lib/nustack_client.js +88 -46
  50. package/dist/lib/onboarding/attach_existing_git.js +7 -7
  51. package/dist/lib/onboarding/binding.js +11 -11
  52. package/dist/lib/onboarding/folder_state.js +10 -10
  53. package/dist/lib/onboarding/handoff_apply.js +2 -2
  54. package/dist/lib/onboarding/init_fresh_folder.js +18 -18
  55. package/dist/lib/onboarding/manifest_writer.js +31 -31
  56. package/dist/lib/onboarding/web_first_attach.js +12 -12
  57. package/dist/lib/{workspace_context.js → project_context.js} +24 -24
  58. package/dist/lib/review/client.js +1 -1
  59. package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
  60. package/dist/lib/{lanes → session-engine}/frames.js +36 -27
  61. package/dist/lib/session-engine/prune.js +233 -0
  62. package/dist/lib/session-engine/registry.js +83 -0
  63. package/dist/lib/session-engine/supervisor.js +321 -0
  64. package/dist/lib/session-engine/worktree.js +89 -0
  65. package/dist/lib/session_telemetry.js +2 -2
  66. package/dist/lib/sessions/client.js +8 -8
  67. package/dist/lib/sessions/outsideSessions.js +6 -6
  68. package/dist/lib/telemetry/config_renderer.js +3 -3
  69. package/dist/lib/telemetry/health_report.js +1 -1
  70. package/package.json +5 -4
  71. package/dist/commands/lane.js +0 -12
  72. package/dist/lib/lanes/supervisor.js +0 -247
  73. package/dist/lib/lanes/worktree.js +0 -33
  74. package/dist/lib/local_store/workspace_events_cache.js +0 -54
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { printJsonSuccess } from "../json_output.js";
5
+ import { loadLogscope } from "./library.js";
6
+ export class LogsNoRepository extends Error {
7
+ code = "LOGS_NO_REPOSITORY";
8
+ constructor(where) {
9
+ super(`not a git repository: ${where} — pass --root <path> or run inside a checkout`);
10
+ this.name = "LogsNoRepository";
11
+ }
12
+ }
13
+ export class LogsUnavailable extends Error {
14
+ code = "LOGS_UNAVAILABLE";
15
+ constructor(reason) {
16
+ super(reason);
17
+ this.name = "LogsUnavailable";
18
+ }
19
+ }
20
+ export class LogsRefused extends Error {
21
+ code = "LOGS_REFUSED";
22
+ constructor(reason) {
23
+ super(reason);
24
+ this.name = "LogsRefused";
25
+ }
26
+ }
27
+ export class LogsNotFound extends Error {
28
+ code = "LOGS_NOT_FOUND";
29
+ constructor(reason) {
30
+ super(reason);
31
+ this.name = "LogsNotFound";
32
+ }
33
+ }
34
+ export function resolveRoot(root, cwd) {
35
+ if (root !== undefined) {
36
+ const path = resolve(cwd, root);
37
+ if (!existsSync(path))
38
+ throw new LogsNoRepository(root);
39
+ return path;
40
+ }
41
+ return gitRoot(cwd);
42
+ }
43
+ function gitRoot(from) {
44
+ try {
45
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
46
+ .toString("utf8")
47
+ .trim();
48
+ }
49
+ catch {
50
+ throw new LogsNoRepository(from);
51
+ }
52
+ }
53
+ export const progress = (line) => {
54
+ process.stderr.write(`${line}\n`);
55
+ };
56
+ export async function answerLogscope(fn, argv, options, cwd) {
57
+ const library = await loadLogscope();
58
+ const known = library.findFn(fn);
59
+ if (known === undefined) {
60
+ throw new LogsUnavailable(`unknown logscope function '${fn}' — run \`nustack logscope functions\` for the list`);
61
+ }
62
+ const args = library.parseArgs(known, argv);
63
+ if (known.args["root"] !== undefined && args["root"] === undefined) {
64
+ args["root"] = resolveRoot(options.root, cwd);
65
+ }
66
+ if (args["limit"] === undefined && options.limit !== undefined)
67
+ args["limit"] = Number(options.limit);
68
+ const limit = args["limit"];
69
+ const budget = options.budget !== undefined ? Number(options.budget) : limit !== undefined ? 0 : library.DEFAULT_BUDGET;
70
+ const result = library.dispatch({ fn: known.name, args, corpus: options.corpus, budget });
71
+ if (result.answer.unavailable !== undefined)
72
+ throw new LogsUnavailable(result.answer.unavailable);
73
+ if (options.json === true) {
74
+ printJsonSuccess(`logscope ${known.name}`, result.answer);
75
+ return;
76
+ }
77
+ console.log(result.rendered);
78
+ }
79
+ export async function listLogscopeFunctions(options) {
80
+ const library = await loadLogscope();
81
+ const budget = options.budget !== undefined ? Number(options.budget) : 0;
82
+ const result = library.dispatch({ fn: "functions", args: {}, budget });
83
+ if (options.json === true) {
84
+ printJsonSuccess("logscope functions", result.answer);
85
+ return;
86
+ }
87
+ console.log(result.rendered);
88
+ console.log([
89
+ "",
90
+ "lifecycle — `nustack logscope <verb>`",
91
+ " allow <targets...> grant a log source; nothing is read before this",
92
+ " allowed the grants covering this project",
93
+ " revoke <targets...> withdraw grants",
94
+ " index <sources...> ingest or refresh a corpus from granted sources",
95
+ " corpora every corpus indexed on this machine",
96
+ " prune [--dry-run] drop every corpus past its retention",
97
+ " forget <id> drop one corpus and its store",
98
+ ].join("\n"));
99
+ }
@@ -0,0 +1,19 @@
1
+ export class LogsLibraryMissing extends Error {
2
+ code = "LOGS_LIBRARY_MISSING";
3
+ constructor() {
4
+ super("the log-tracking library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/logscope`) or run nustack from a checkout of the monorepo.");
5
+ this.name = "LogsLibraryMissing";
6
+ }
7
+ }
8
+ let cached = null;
9
+ export async function loadLogscope() {
10
+ if (cached !== null)
11
+ return cached;
12
+ try {
13
+ cached = (await import("@nurix/logscope"));
14
+ return cached;
15
+ }
16
+ catch {
17
+ throw new LogsLibraryMissing();
18
+ }
19
+ }
@@ -0,0 +1,159 @@
1
+ import { printJsonSuccess } from "../json_output.js";
2
+ import { loadLogscope } from "./library.js";
3
+ import { LogsNotFound, LogsRefused, progress, resolveRoot } from "./answer.js";
4
+ function reportPruneToStderr(library) {
5
+ for (const record of library.prune()) {
6
+ progress(`pruned ${record.id} (retention ${library.retentionOf(record)} days, indexed ${record.indexedAt ?? "unknown"})`);
7
+ }
8
+ }
9
+ export async function indexLogs(targets, options, cwd) {
10
+ const library = await loadLogscope();
11
+ const root = resolveRoot(options.root, cwd);
12
+ const retentionDays = Number(options.retain ?? String(library.DEFAULT_RETENTION_DAYS));
13
+ if (!Number.isInteger(retentionDays) || retentionDays < 0) {
14
+ throw new LogsRefused("--retain takes a non-negative whole number of days");
15
+ }
16
+ reportPruneToStderr(library);
17
+ const outcome = await library.indexCorpus({
18
+ targets,
19
+ root,
20
+ since: options.since,
21
+ until: options.until,
22
+ revision: options.revision,
23
+ redact: options.redact !== false,
24
+ keepExamples: options.examples === true,
25
+ retentionDays,
26
+ });
27
+ if (!outcome.ok)
28
+ throw new LogsRefused(outcome.message);
29
+ if (options.json === true) {
30
+ printJsonSuccess("logscope index", {
31
+ id: outcome.id,
32
+ sources: outcome.sources,
33
+ events: outcome.events,
34
+ templates: outcome.templates,
35
+ ratio: outcome.ratio,
36
+ resumes: outcome.resumes,
37
+ retentionDays: outcome.retentionDays,
38
+ hasExamples: outcome.hasExamples,
39
+ expiresAt: outcome.expiresAt ?? null,
40
+ warning: outcome.warning ?? null,
41
+ });
42
+ return;
43
+ }
44
+ console.log([
45
+ `indexed ${outcome.id}`,
46
+ ` ${outcome.events} events -> ${outcome.templates} templates (ratio ${outcome.ratio.toFixed(4)})`,
47
+ ` sources ${Object.entries(outcome.resumes).map(([path, resume]) => `${path} [${resume}]`).join(", ")}`,
48
+ ` retention ${outcome.retentionDays === 0 ? "off (kept until forgotten)" : `${outcome.retentionDays} days (expires ${outcome.expiresAt ?? "unknown"})`}`,
49
+ ` examples ${outcome.hasExamples ? "kept" : "not kept — drill and trace <id> cannot answer on this corpus"}`,
50
+ ...(outcome.warning === undefined ? [] : [` WARNING: ${outcome.warning}`]),
51
+ ].join("\n"));
52
+ }
53
+ export async function listCorpora(options) {
54
+ const library = await loadLogscope();
55
+ reportPruneToStderr(library);
56
+ const corpora = library.readRegistry().map((record) => ({
57
+ id: record.id,
58
+ sources: record.sources,
59
+ events: record.events ?? 0,
60
+ templates: record.templates ?? 0,
61
+ indexedAt: record.indexedAt ?? null,
62
+ retentionDays: library.retentionOf(record),
63
+ expiresAt: library.expiresAt(record) ?? null,
64
+ hasExamples: record.hasExamples === true,
65
+ }));
66
+ if (options.json === true) {
67
+ printJsonSuccess("logscope corpora", { corpora });
68
+ return;
69
+ }
70
+ if (corpora.length === 0) {
71
+ console.log("no corpus has been indexed on this machine — run: nustack logscope index <sources...>");
72
+ return;
73
+ }
74
+ console.log(corpora
75
+ .map((corpus) => `${corpus.id} ${corpus.events} events, ${corpus.templates} templates (${corpus.sources.length} sources) ` +
76
+ (corpus.retentionDays === 0
77
+ ? "retention off"
78
+ : `retention ${corpus.retentionDays} days, expires ${corpus.expiresAt ?? "unknown"}`))
79
+ .join("\n"));
80
+ }
81
+ export async function allowSources(targets, options, cwd) {
82
+ const library = await loadLogscope();
83
+ const root = resolveRoot(options.root, cwd);
84
+ const allowed = library.allow(root, targets, options.note).map((grant) => grant.path);
85
+ if (options.json === true) {
86
+ printJsonSuccess("logscope allow", { root, allowed });
87
+ return;
88
+ }
89
+ console.log(allowed.map((path) => `allowed ${path}`).join("\n"));
90
+ }
91
+ export async function listAllowed(options, cwd) {
92
+ const library = await loadLogscope();
93
+ const root = resolveRoot(options.root, cwd);
94
+ const grants = library.allowedFor(root);
95
+ if (options.json === true) {
96
+ printJsonSuccess("logscope allowed", { root, grants });
97
+ return;
98
+ }
99
+ if (grants.length === 0) {
100
+ console.log(`no source is allowed for ${root} — run: nustack logscope allow <path...>`);
101
+ return;
102
+ }
103
+ console.log(grants
104
+ .map((grant) => `${grant.path} granted ${grant.grantedAt}${grant.note === undefined ? "" : ` ${grant.note}`}`)
105
+ .join("\n"));
106
+ }
107
+ export async function revokeSources(targets, options, cwd) {
108
+ const library = await loadLogscope();
109
+ const root = resolveRoot(options.root, cwd);
110
+ const revoked = library.revoke(root, targets);
111
+ if (revoked === 0)
112
+ throw new LogsNotFound(`no grant under ${root} names ${targets.join(", ")}`);
113
+ if (options.json === true) {
114
+ printJsonSuccess("logscope revoke", { root, revoked });
115
+ return;
116
+ }
117
+ console.log(`revoked ${revoked} grant(s)`);
118
+ }
119
+ export async function pruneCorpora(options, cwd) {
120
+ const library = await loadLogscope();
121
+ resolveRoot(options.root, cwd);
122
+ const dryRun = options.dryRun === true;
123
+ const now = new Date();
124
+ const candidates = dryRun
125
+ ? library.readRegistry().filter((record) => {
126
+ const at = library.expiresAt(record);
127
+ return at !== null && at !== undefined && new Date(at).getTime() <= now.getTime();
128
+ })
129
+ : library.prune();
130
+ const pruned = candidates.map((record) => ({
131
+ id: record.id,
132
+ retentionDays: library.retentionOf(record),
133
+ indexedAt: record.indexedAt ?? null,
134
+ }));
135
+ if (options.json === true) {
136
+ printJsonSuccess("logscope prune", { dryRun, pruned });
137
+ return;
138
+ }
139
+ if (pruned.length === 0) {
140
+ console.log("nothing to prune");
141
+ return;
142
+ }
143
+ console.log(pruned
144
+ .map((record) => `${dryRun ? "would prune" : "pruned"} ${record.id} (retention ${record.retentionDays} days, indexed ${record.indexedAt ?? "unknown"})`)
145
+ .join("\n"));
146
+ }
147
+ export async function forgetCorpusCommand(ref, options) {
148
+ const library = await loadLogscope();
149
+ if (ref === undefined)
150
+ throw new LogsNotFound("name the corpus to forget — run: nustack logscope corpora");
151
+ const forgot = library.forgetCorpus(ref);
152
+ if (forgot === undefined)
153
+ throw new LogsNotFound(`no corpus matches ${ref}`);
154
+ if (options.json === true) {
155
+ printJsonSuccess("logscope forget", { forgot });
156
+ return;
157
+ }
158
+ console.log(`forgot ${forgot}`);
159
+ }
@@ -1,6 +1,6 @@
1
1
  import { assertSafeNuStackUrl, assertSafeOpaqueId } from "./guards.js";
2
2
  import { createSseParser } from "./sse_framing.js";
3
- import { DEVICE_HEADER, knownDeviceId, refreshAccessTokenAfterRefusal, userAgent } from "./auth_grant.js";
3
+ import { DEVICE_HEADER, envToken, knownDeviceId, refreshAccessTokenAfterRefusal, TokenRefusedError, userAgent } from "./auth_grant.js";
4
4
  import { getOrCreateDeviceId } from "./device_identity.js";
5
5
  const DEFAULT_NUSTACK_URL = "https://apps.nustack.ai";
6
6
  const REQUEST_TIMEOUT_MS = 10_000;
@@ -12,6 +12,8 @@ async function authorizedFetch(input, init = {}) {
12
12
  const bearer = headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
13
13
  if (response.status !== 401 || !bearer || isStreamBody(init.body))
14
14
  return response;
15
+ if (envToken())
16
+ throw new TokenRefusedError();
15
17
  const fresh = await refreshAccessTokenAfterRefusal(bearer).catch(() => null);
16
18
  if (!fresh)
17
19
  return response;
@@ -60,7 +62,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
60
62
  const response = await authorizedFetch(`${baseUrl}/api/v1/keys/exchange`, {
61
63
  method: "POST",
62
64
  headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
63
- body: JSON.stringify({ workspaceId: params.workspaceId, service: params.service }),
65
+ body: JSON.stringify({ projectId: params.projectId, service: params.service }),
64
66
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
65
67
  });
66
68
  const payload = (await response.json().catch(() => null));
@@ -71,7 +73,7 @@ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
71
73
  }
72
74
  export async function fetchServiceCredential(baseUrl, accessToken, params) {
73
75
  const url = new URL(`${baseUrl}/api/v1/keys/credentials`);
74
- url.searchParams.set("workspaceId", assertSafeOpaqueId("The Workspace id", params.workspaceId));
76
+ url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", params.projectId));
75
77
  url.searchParams.set("service", assertSafeOpaqueId("The Service slug", params.service));
76
78
  const response = await authorizedFetch(url, {
77
79
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -83,9 +85,9 @@ export async function fetchServiceCredential(baseUrl, accessToken, params) {
83
85
  }
84
86
  return narrowProjection(payload, params.service);
85
87
  }
86
- export async function listWorkspaceIntegrations(baseUrl, accessToken, workspaceId) {
88
+ export async function listProjectIntegrations(baseUrl, accessToken, projectId) {
87
89
  const url = new URL(`${baseUrl}/api/v1/keys/integrations`);
88
- url.searchParams.set("workspaceId", assertSafeOpaqueId("The Workspace id", workspaceId));
90
+ url.searchParams.set("projectId", assertSafeOpaqueId("The Project id", projectId));
89
91
  const response = await authorizedFetch(url, {
90
92
  headers: { Authorization: `Bearer ${accessToken}` },
91
93
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -101,7 +103,7 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
101
103
  const response = await authorizedFetch(`${baseUrl}/api/v1/keys/broker/${encodeURIComponent(service)}`, {
102
104
  method: "POST",
103
105
  headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
104
- body: JSON.stringify({ workspaceId: params.workspaceId }),
106
+ body: JSON.stringify({ projectId: params.projectId }),
105
107
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
106
108
  });
107
109
  const payload = (await response.json().catch(() => null));
@@ -110,8 +112,8 @@ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
110
112
  }
111
113
  return { url: payload.url ?? null, key: payload.key, meta: payload.meta ?? null, grantId: payload.grantId };
112
114
  }
113
- export async function searchWorkspaces(baseUrl, query, opts = {}) {
114
- const url = new URL(`${baseUrl}/public/v1/workspaces`);
115
+ export async function searchProjects(baseUrl, query, opts = {}) {
116
+ const url = new URL(`${baseUrl}/public/v1/projects`);
115
117
  url.searchParams.set("q", query);
116
118
  if (opts.category)
117
119
  url.searchParams.set("category", opts.category);
@@ -120,7 +122,7 @@ export async function searchWorkspaces(baseUrl, query, opts = {}) {
120
122
  const response = await authorizedFetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
121
123
  const payload = (await response.json().catch(() => null));
122
124
  if (!response.ok || !Array.isArray(payload?.data?.items)) {
123
- throw new Error(errorMessage(payload, response.status, "workspace search failed"));
125
+ throw new Error(errorMessage(payload, response.status, "project search failed"));
124
126
  }
125
127
  return payload.data.items;
126
128
  }
@@ -190,10 +192,10 @@ export async function acknowledgeBindingCursor(baseUrl, accessToken, params) {
190
192
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
191
193
  });
192
194
  const payload = (await response.json().catch(() => null));
193
- if (!response.ok || typeof payload?.data?.lastAppliedWorkspaceSequence !== "number") {
195
+ if (!response.ok || typeof payload?.data?.lastAppliedProjectSequence !== "number") {
194
196
  throw new Error(errorMessage(payload, response.status, "cursor acknowledgement failed"));
195
197
  }
196
- return payload.data.lastAppliedWorkspaceSequence;
198
+ return payload.data.lastAppliedProjectSequence;
197
199
  }
198
200
  export async function fetchSyncWhitelist(baseUrl, accessToken, bindingId) {
199
201
  const safeId = assertSafeOpaqueId("The binding id", bindingId);
@@ -282,6 +284,46 @@ export async function listOrganizations(baseUrl, accessToken) {
282
284
  }
283
285
  return payload.data.items;
284
286
  }
287
+ export class PersonalTokenRequestError extends Error {
288
+ code;
289
+ constructor(code, message) {
290
+ super(message);
291
+ this.code = code;
292
+ this.name = "PersonalTokenRequestError";
293
+ }
294
+ }
295
+ async function readTokenEnvelope(response, label) {
296
+ const payload = (await response.json().catch(() => null));
297
+ if (!response.ok || payload?.data === undefined) {
298
+ throw new PersonalTokenRequestError(payload?.error?.code ?? "REQUEST_FAILED", errorMessage(payload, response.status, label));
299
+ }
300
+ return payload.data;
301
+ }
302
+ export async function listPersonalTokens(baseUrl, accessToken) {
303
+ const response = await authorizedFetch(`${baseUrl}/api/v2/tokens`, {
304
+ headers: authHeaders(accessToken),
305
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
306
+ });
307
+ const data = await readTokenEnvelope(response, "token list failed");
308
+ return Array.isArray(data.items) ? data.items : [];
309
+ }
310
+ export async function createPersonalToken(baseUrl, accessToken, params) {
311
+ const response = await authorizedFetch(`${baseUrl}/api/v2/tokens`, {
312
+ method: "POST",
313
+ headers: { ...authHeaders(accessToken), "Content-Type": "application/json" },
314
+ body: JSON.stringify(params),
315
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
316
+ });
317
+ return readTokenEnvelope(response, "token create failed");
318
+ }
319
+ export async function revokePersonalToken(baseUrl, accessToken, tokenId) {
320
+ const response = await authorizedFetch(`${baseUrl}/api/v2/tokens/${encodeURIComponent(assertSafeOpaqueId("The token id", tokenId))}`, {
321
+ method: "DELETE",
322
+ headers: authHeaders(accessToken),
323
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
324
+ });
325
+ return readTokenEnvelope(response, "token revoke failed");
326
+ }
285
327
  export async function createOrganization(baseUrl, accessToken, params, idempotencyKey) {
286
328
  const response = await authorizedFetch(`${baseUrl}/api/v2/organizations`, {
287
329
  method: "POST",
@@ -295,31 +337,31 @@ export async function createOrganization(baseUrl, accessToken, params, idempoten
295
337
  }
296
338
  return payload.data;
297
339
  }
298
- export async function listWorkspaces(baseUrl, accessToken, organizationId) {
340
+ export async function listProjects(baseUrl, accessToken, organizationId) {
299
341
  const safeId = assertSafeOpaqueId("The Organization id", organizationId);
300
- const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces`, {
342
+ const response = await authorizedFetch(`${baseUrl}/api/v2/projects`, {
301
343
  headers: { ...authHeaders(accessToken), "X-NuStack-Organization": safeId },
302
344
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
303
345
  });
304
346
  const payload = (await response.json().catch(() => null));
305
347
  if (!response.ok || !Array.isArray(payload?.data?.items)) {
306
- throw new Error(errorMessage(payload, response.status, "workspace list failed"));
348
+ throw new Error(errorMessage(payload, response.status, "project list failed"));
307
349
  }
308
350
  return payload.data.items;
309
351
  }
310
- export async function getWorkspace(baseUrl, accessToken, workspaceId) {
311
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
312
- const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}`, {
352
+ export async function getProject(baseUrl, accessToken, projectId) {
353
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
354
+ const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}`, {
313
355
  headers: authHeaders(accessToken),
314
356
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
315
357
  });
316
358
  const payload = (await response.json().catch(() => null));
317
359
  if (!response.ok || !payload?.data?.id) {
318
- throw new Error(errorMessage(payload, response.status, "workspace fetch failed"));
360
+ throw new Error(errorMessage(payload, response.status, "project fetch failed"));
319
361
  }
320
362
  return payload.data;
321
363
  }
322
- export function mintWorkspaceId() {
364
+ export function mintProjectId() {
323
365
  const bytes = crypto.getRandomValues(new Uint8Array(16));
324
366
  const ms = Date.now();
325
367
  bytes[0] = Math.floor(ms / 2 ** 40) & 0xff;
@@ -360,9 +402,9 @@ async function* readSseFrames(body) {
360
402
  for (const block of parser.flush())
361
403
  yield toFrame(block);
362
404
  }
363
- export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId, prompt, opts = {}) {
405
+ export async function createProjectViaAsk(baseUrl, accessToken, organizationId, prompt, opts = {}) {
364
406
  const safeOrganization = assertSafeOpaqueId("The Organization id", organizationId);
365
- const workspaceId = opts.workspaceId ?? mintWorkspaceId();
407
+ const projectId = opts.projectId ?? mintProjectId();
366
408
  const fetchImpl = opts.fetchImpl ?? fetch;
367
409
  const response = await fetchImpl(`${baseUrl}/api/v2/ask`, {
368
410
  method: "POST",
@@ -371,11 +413,11 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
371
413
  "Content-Type": "application/json",
372
414
  "X-NuStack-Organization": safeOrganization,
373
415
  },
374
- body: JSON.stringify({ prompt, workspaceId }),
416
+ body: JSON.stringify({ prompt, projectId }),
375
417
  });
376
418
  if (!response.ok || !response.body) {
377
419
  const payload = (await response.json().catch(() => null));
378
- throw new Error(errorMessage(payload, response.status, "workspace create failed"));
420
+ throw new Error(errorMessage(payload, response.status, "project create failed"));
379
421
  }
380
422
  let started = false;
381
423
  let turnId = null;
@@ -384,7 +426,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
384
426
  if (frame.event === "start") {
385
427
  started = true;
386
428
  turnId = typeof frame.data.turnId === "string" ? frame.data.turnId : null;
387
- opts.onPhase?.("Workspace created — writing the first document");
429
+ opts.onPhase?.("Project created — writing the first document");
388
430
  }
389
431
  else if (frame.event === "thinking" && typeof frame.data.message === "string") {
390
432
  opts.onPhase?.(frame.data.message);
@@ -392,7 +434,7 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
392
434
  else if (frame.event === "error") {
393
435
  const message = typeof frame.data.message === "string" ? frame.data.message : "generation failed";
394
436
  if (!started)
395
- throw new Error(`workspace create failed: ${message}`);
437
+ throw new Error(`project create failed: ${message}`);
396
438
  generationError = message;
397
439
  break;
398
440
  }
@@ -401,12 +443,12 @@ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId
401
443
  }
402
444
  }
403
445
  if (!started)
404
- throw new Error("workspace create failed: the stream ended before the Workspace was reported");
405
- return { workspaceId, turnId, generationError };
446
+ throw new Error("project create failed: the stream ended before the Project was reported");
447
+ return { projectId, turnId, generationError };
406
448
  }
407
- async function postWorkspacePublication(baseUrl, accessToken, workspaceId, action, body, idempotencyKey) {
408
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
409
- const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/${action}`, {
449
+ async function postProjectPublication(baseUrl, accessToken, projectId, action, body, idempotencyKey) {
450
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
451
+ const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/${action}`, {
410
452
  method: "POST",
411
453
  headers: mutationHeaders(accessToken, idempotencyKey),
412
454
  body: JSON.stringify(body),
@@ -414,19 +456,19 @@ async function postWorkspacePublication(baseUrl, accessToken, workspaceId, actio
414
456
  });
415
457
  const payload = (await response.json().catch(() => null));
416
458
  if (!response.ok || payload?.data === undefined) {
417
- throw new Error(errorMessage(payload, response.status, `workspace ${action} failed`));
459
+ throw new Error(errorMessage(payload, response.status, `project ${action} failed`));
418
460
  }
419
461
  return payload.data;
420
462
  }
421
- export async function publishWorkspace(baseUrl, accessToken, workspaceId, profile, idempotencyKey) {
422
- return postWorkspacePublication(baseUrl, accessToken, workspaceId, "publish", profile, idempotencyKey);
463
+ export async function publishProject(baseUrl, accessToken, projectId, profile, idempotencyKey) {
464
+ return postProjectPublication(baseUrl, accessToken, projectId, "publish", profile, idempotencyKey);
423
465
  }
424
- export async function unpublishWorkspace(baseUrl, accessToken, workspaceId, idempotencyKey) {
425
- return postWorkspacePublication(baseUrl, accessToken, workspaceId, "unpublish", {}, idempotencyKey);
466
+ export async function unpublishProject(baseUrl, accessToken, projectId, idempotencyKey) {
467
+ return postProjectPublication(baseUrl, accessToken, projectId, "unpublish", {}, idempotencyKey);
426
468
  }
427
- export async function listDocuments(baseUrl, accessToken, workspaceId) {
428
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
429
- const response = await authorizedFetch(`${baseUrl}/api/v2/documents?workspaceId=${encodeURIComponent(safeId)}`, {
469
+ export async function listDocuments(baseUrl, accessToken, projectId) {
470
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
471
+ const response = await authorizedFetch(`${baseUrl}/api/v2/documents?projectId=${encodeURIComponent(safeId)}`, {
430
472
  headers: authHeaders(accessToken),
431
473
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
432
474
  });
@@ -448,9 +490,9 @@ export async function getDocument(baseUrl, accessToken, documentId) {
448
490
  }
449
491
  return payload.data;
450
492
  }
451
- export async function listHandoffs(baseUrl, accessToken, workspaceId) {
452
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
453
- const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/handoffs`, {
493
+ export async function listHandoffs(baseUrl, accessToken, projectId) {
494
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
495
+ const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/handoffs`, {
454
496
  headers: authHeaders(accessToken),
455
497
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
456
498
  });
@@ -472,13 +514,13 @@ export async function getHandoff(baseUrl, accessToken, handoffId) {
472
514
  }
473
515
  return payload.data;
474
516
  }
475
- export async function latestHandoff(baseUrl, accessToken, workspaceId) {
476
- const handoffs = await listHandoffs(baseUrl, accessToken, workspaceId);
517
+ export async function latestHandoff(baseUrl, accessToken, projectId) {
518
+ const handoffs = await listHandoffs(baseUrl, accessToken, projectId);
477
519
  return handoffs.length > 0 ? handoffs[0] : null;
478
520
  }
479
- export async function createBinding(baseUrl, accessToken, workspaceId, input, idempotencyKey) {
480
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
481
- const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/bindings`, {
521
+ export async function createBinding(baseUrl, accessToken, projectId, input, idempotencyKey) {
522
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
523
+ const response = await authorizedFetch(`${baseUrl}/api/v2/projects/${encodeURIComponent(safeId)}/bindings`, {
482
524
  method: "POST",
483
525
  headers: mutationHeaders(accessToken, idempotencyKey),
484
526
  body: JSON.stringify({
@@ -7,24 +7,24 @@ import { runRepositoryOnboarding, } from "./binding.js";
7
7
  export async function attachExistingGit(ctx, options) {
8
8
  const state = classifyFolder(ctx.cwd);
9
9
  if (state.class !== "git_worktree" || !state.repository || !state.git) {
10
- return { ok: false, code: "NOT_A_WORKTREE", message: "`nustack workspace attach` needs an existing Git worktree — run `nustack init` to establish one." };
10
+ return { ok: false, code: "NOT_A_WORKTREE", message: "`nustack project attach` needs an existing Git worktree — run `nustack init` to establish one." };
11
11
  }
12
12
  const rejection = attachRejection(state.git);
13
13
  if (rejection) {
14
14
  return { ok: false, code: rejection.toUpperCase(), message: rejectionMessage(rejection) };
15
15
  }
16
- if (state.git.markerWorkspaceId && state.git.markerWorkspaceId !== options.workspaceId && !options.confirmRebind) {
16
+ if (state.git.markerProjectId && state.git.markerProjectId !== options.projectId && !options.confirmRebind) {
17
17
  return {
18
18
  ok: false,
19
19
  code: "REBIND_CONFIRMATION_REQUIRED",
20
- message: `This repository is bound to Workspace ${state.git.markerWorkspaceId}. Re-run with confirmation to rebind it to ${options.workspaceId}.`,
20
+ message: `This repository is bound to Project ${state.git.markerProjectId}. Re-run with confirmation to rebind it to ${options.projectId}.`,
21
21
  };
22
22
  }
23
23
  const boundBranch = readSymbolicBranch(state.repository);
24
24
  if (!boundBranch) {
25
25
  return { ok: false, code: "DETACHED_HEAD", message: "HEAD is detached — check out a branch before attaching." };
26
26
  }
27
- const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.workspaceId);
27
+ const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.projectId);
28
28
  const baselineCommitId = readHeadCommit(state.repository);
29
29
  const result = await runRepositoryOnboarding({
30
30
  store: ctx.store,
@@ -32,7 +32,7 @@ export async function attachExistingGit(ctx, options) {
32
32
  accessToken: ctx.accessToken,
33
33
  deviceRef: ctx.deviceRef,
34
34
  flow: "existing_git",
35
- workspaceId: options.workspaceId,
35
+ projectId: options.projectId,
36
36
  organizationId: options.organizationId,
37
37
  repository: state.repository,
38
38
  boundBranch,
@@ -55,8 +55,8 @@ async function reconcileLatest(ctx, bindingId) {
55
55
  }
56
56
  function rejectionMessage(rejection) {
57
57
  switch (rejection) {
58
- case "nested_workspace":
59
- return "This repository is nested inside another NuStack Workspace — one Workspace has one logical repository root (F-014).";
58
+ case "nested_project":
59
+ return "This repository is nested inside another NuStack Project — one Project has one logical repository root (F-014).";
60
60
  case "operation_in_progress":
61
61
  return "A merge, rebase, cherry-pick, or revert is in progress — finish or abort it, then re-attach.";
62
62
  case "detached_head":