@uniqbit/mate-core 0.17.0-canary.10 → 0.17.0-canary.11

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 (66) hide show
  1. package/package.json +6 -1
  2. package/src/cli/commands/artifact/finish/index.ts +0 -9
  3. package/src/cli/commands/artifact/finish/openspec.ts +2 -2
  4. package/src/cli/commands/artifact/finish/registry.ts +1 -1
  5. package/src/cli/commands/artifact/pending/discovery.ts +1 -1
  6. package/src/cli/commands/artifact/pending/index.ts +0 -15
  7. package/src/cli/commands/cap/graphify.ts +1 -1
  8. package/src/cli/commands/companion/hub.ts +2 -1
  9. package/src/cli/commands/companion/list.ts +2 -1
  10. package/src/cli/commands/companion/prepare.ts +2 -2
  11. package/src/cli/commands/doctor.ts +2 -1
  12. package/src/cli/commands/launch/shared.ts +2 -1
  13. package/src/cli/commands/report/highlight.ts +1 -4
  14. package/src/cli/commands/report/renderer.ts +0 -2
  15. package/src/cli/commands/studio/access.ts +203 -0
  16. package/src/cli/commands/studio/index.ts +207 -26
  17. package/src/cli/commands/studio/openspec-cli.ts +1 -1
  18. package/src/cli/commands/studio/selection.ts +1 -1
  19. package/src/cli/commands/studio/server.ts +281 -18
  20. package/src/cli/commands/studio/terminal-assets.ts +50 -0
  21. package/src/cli/commands/studio/terminal-launch.ts +71 -0
  22. package/src/cli/commands/studio/terminal.ts +593 -0
  23. package/src/cli/commands/studio/vault.ts +1 -6
  24. package/src/cli/commands/studio/views/document.tsx +12 -2
  25. package/src/cli/commands/studio/views/model.ts +10 -0
  26. package/src/cli/commands/studio/views/styles.ts +11 -0
  27. package/src/cli/commands/studio/views/terminal.tsx +232 -0
  28. package/src/cli/companion-link-wizard.tsx +1 -1
  29. package/src/cli/companion-selector.tsx +1 -1
  30. package/src/cli/launch-selector.tsx +1 -1
  31. package/src/cli/usage.ts +4 -2
  32. package/src/cli/write-json-stdout.ts +2 -2
  33. package/src/lib/components/spinner.tsx +2 -2
  34. package/src/lib/components/startup-progress.tsx +1 -1
  35. package/src/lib/context-mode-package.ts +1 -1
  36. package/src/lib/orchestrator/companion-hub.ts +3 -3
  37. package/src/lib/orchestrator/companion-registry-store.ts +1 -1
  38. package/src/lib/orchestrator/config-store.ts +2 -2
  39. package/src/lib/orchestrator/editor.ts +4 -12
  40. package/src/lib/orchestrator/projection-entries.ts +1 -1
  41. package/src/lib/orchestrator/projection-record.ts +1 -1
  42. package/src/lib/orchestrator/projection-types.ts +0 -4
  43. package/src/lib/orchestrator/repo-local-registry.ts +0 -1
  44. package/src/lib/orchestrator/root-context.ts +1 -1
  45. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  46. package/src/lib/orchestrator/setup-preflight.ts +1 -1
  47. package/src/lib/orchestrator/types.ts +1 -1
  48. package/src/lib/orchestrator/working-repo-projection.ts +1 -1
  49. package/src/lib/orchestrator/workspace-inventory.ts +2 -2
  50. package/src/lib/package-paths.ts +1 -11
  51. package/src/lib/prebuilt-workspace.ts +1 -1
  52. package/src/lib/public-npm.ts +1 -1
  53. package/src/playbooks/companion-guidance.ts +1 -28
  54. package/src/runtime/companion-sync.ts +0 -3
  55. package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
  56. package/src/tools/setup/capabilities/graphify.ts +2 -2
  57. package/src/tools/setup/capabilities/openspec.ts +2 -7
  58. package/src/tools/setup/capabilities/react-doctor.ts +1 -1
  59. package/src/tools/setup/capabilities/rtk.ts +2 -2
  60. package/src/tools/setup/context-services.ts +3 -6
  61. package/src/tools/setup/mate.ts +1 -1
  62. package/src/tools/setup/providers/claude-format.ts +1 -1
  63. package/src/tools/setup/providers/claude.ts +1 -1
  64. package/src/tools/setup/providers/opencode.ts +0 -2
  65. package/src/tools/setup/runtime-documents.ts +1 -7
  66. package/src/tools/setup.ts +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.17.0-canary.10",
3
+ "version": "0.17.0-canary.11",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -36,6 +36,11 @@
36
36
  "@opentui/keymap": "^0.4.5",
37
37
  "@opentui/solid": "^0.4.5",
38
38
  "@tanstack/highlight": "0.0.10",
39
+ "@xterm/addon-clipboard": "^0.2.0",
40
+ "@xterm/addon-fit": "^0.11.0",
41
+ "@xterm/addon-unicode11": "^0.9.0",
42
+ "@xterm/addon-web-links": "^0.12.0",
43
+ "@xterm/xterm": "^6.0.0",
39
44
  "hono": "^4.13.5",
40
45
  "ink": "^7.1.0",
41
46
  "react": "^19.2.7",
@@ -1,10 +1 @@
1
1
  export { runArtifactPublishCommand } from "./command";
2
- export type { PublishCommandDeps } from "./command";
3
- export type { FinishResult, FinishStep, FinishStatus } from "./engine";
4
- export type {
5
- ArtifactFinisher,
6
- FinishContext,
7
- FinisherFactory,
8
- Produced,
9
- ResolveResult,
10
- } from "./finisher";
@@ -269,7 +269,7 @@ async function resolved(
269
269
  }
270
270
 
271
271
  /** Tag namespace segregating spec publications from dated change anchors. */
272
- export const SPEC_TAG_NAMESPACE = "openspec/specs";
272
+ const SPEC_TAG_NAMESPACE = "openspec/specs";
273
273
  /** Spec ids named individually before the remainder collapses into a count. */
274
274
  const SPEC_LABEL_LIMIT = 3;
275
275
 
@@ -297,7 +297,7 @@ function specLabel(paths: string[]): string {
297
297
  }
298
298
 
299
299
  /** Commit subject for a spec publication; it belongs to no change, so it names specs, never an anchor. */
300
- export function specCommitSubject(paths: string[]): string {
300
+ function specCommitSubject(paths: string[]): string {
301
301
  const ids = specIds(paths);
302
302
  const named = ids.slice(0, SPEC_LABEL_LIMIT);
303
303
  const remaining = ids.length - named.length;
@@ -5,7 +5,7 @@ import { openspecFinisher } from "./openspec";
5
5
  * Artifact-kind → finisher factory. openspec is the only kind for now; a future ADR
6
6
  * finisher registers here and is selected via `--type`, with no engine changes.
7
7
  */
8
- export const FINISHERS: Record<string, FinisherFactory> = {
8
+ const FINISHERS: Record<string, FinisherFactory> = {
9
9
  openspec: openspecFinisher,
10
10
  };
11
11
 
@@ -52,7 +52,7 @@ export interface ArchiveEntry {
52
52
  const ARCHIVE_ANCHOR_PATTERN = /^\d{4}-\d{2}-\d{2}-.+$/;
53
53
  const ARCHIVE_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
54
54
 
55
- export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
55
+ const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
56
56
  export const SPECS_RELATIVE_DIR = "openspec/specs";
57
57
 
58
58
  /** The tag `mate artifact publish` creates, never a recomputed date. */
@@ -1,17 +1,2 @@
1
1
  export { runArtifactPendingCommand } from "./command";
2
2
  export type { PendingCommandDeps, PendingResult } from "./command";
3
- export {
4
- ARCHIVE_RELATIVE_DIR,
5
- SPECS_RELATIVE_DIR,
6
- discoverArchives,
7
- finishMarker,
8
- pendingArchives,
9
- unattributedSpecs,
10
- } from "./discovery";
11
- export type {
12
- ArchiveEntry,
13
- CommitState,
14
- SpecAttribution,
15
- UnattributedSpec,
16
- UncommittedSpecChange,
17
- } from "./discovery";
@@ -29,7 +29,7 @@ const GRAPHIFY_PATH_COMMANDS = new Set([
29
29
 
30
30
  const GRAPHIFY_GRAPH_COMMANDS = new Set(["query", "path", "explain", "affected", "tree"]);
31
31
 
32
- export function deriveGraphifyPaths(companionPath: string, repositoryId: string): GraphifyPaths {
32
+ function deriveGraphifyPaths(companionPath: string, repositoryId: string): GraphifyPaths {
33
33
  const graphifyOut = path.join(
34
34
  companionPath,
35
35
  GRAPHIFY_STORE_SEGMENT,
@@ -8,6 +8,7 @@ import {
8
8
  updateHubPlugins,
9
9
  } from "../../../lib/orchestrator/companion-hub";
10
10
  import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
11
+ import { writeJsonStdout } from "../../write-json-stdout";
11
12
 
12
13
  function positionalArgs(argv: string[]): string[] {
13
14
  const values: string[] = [];
@@ -71,7 +72,7 @@ async function runHubSync(argv: string[]): Promise<void> {
71
72
  const results = await syncHub(process.cwd());
72
73
  const plugins = await updateHubPlugins(process.cwd());
73
74
  if (argv.includes("--json")) {
74
- console.log(JSON.stringify({ companions: results, plugins }));
75
+ await writeJsonStdout({ companions: results, plugins }, { space: 0 });
75
76
  return;
76
77
  }
77
78
  for (const result of results) console.log(`${result.id}: ${result.status} (${result.message})`);
@@ -1,6 +1,7 @@
1
1
  import { CompanionStore } from "../../../lib/orchestrator/companion-store";
2
2
  import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
3
3
  import { renderRepoListTable } from "../../repo-list-table";
4
+ import { writeJsonStdout } from "../../write-json-stdout";
4
5
 
5
6
  /**
6
7
  * @command mate companion list
@@ -23,7 +24,7 @@ export async function runCompanionListCommand(argv: string[] = []): Promise<void
23
24
  const forceJson = argv.includes("--json");
24
25
 
25
26
  if (forceJson || !process.stdout.isTTY) {
26
- console.log(JSON.stringify({ repositories }, null, 2));
27
+ await writeJsonStdout({ repositories });
27
28
  return;
28
29
  }
29
30
 
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { FRAMEWORK_NAME } from "../../../framework";
4
4
  import { preparePrebuiltWorkspace } from "../../../lib/prebuilt-workspace-prepare";
5
5
 
6
- export const companionPrepareCommandDeps = {
6
+ const companionPrepareCommandDeps = {
7
7
  preparePrebuiltWorkspace,
8
8
  };
9
9
 
@@ -13,7 +13,7 @@ interface ParsedPrepareArgs {
13
13
  error?: string;
14
14
  }
15
15
 
16
- export function parsePrepareArgs(argv: string[]): ParsedPrepareArgs {
16
+ function parsePrepareArgs(argv: string[]): ParsedPrepareArgs {
17
17
  let bundle: string | undefined;
18
18
  const positional: string[] = [];
19
19
 
@@ -10,6 +10,7 @@ import { resolveRootContext, type RootContext } from "../../lib/orchestrator/roo
10
10
  import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
11
11
  import { getRequiredPluginDrift } from "../../tools/setup/policy";
12
12
  import { resolveCommandOnPath } from "../../tools/setup/utils";
13
+ import { writeJsonStdout } from "../write-json-stdout";
13
14
  import { printSection, renderKeyValueTable, renderTable } from "./status";
14
15
 
15
16
  interface DoctorDeps {
@@ -354,7 +355,7 @@ function renderHumanReport(report: DoctorReport): void {
354
355
  export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
355
356
  const report = await collectDoctorReport(deps);
356
357
  if (argv.includes("--json")) {
357
- console.log(JSON.stringify(report, null, 2));
358
+ await writeJsonStdout(report);
358
359
  return;
359
360
  }
360
361
  renderHumanReport(report);
@@ -10,6 +10,7 @@ import { createStartupProgress } from "../../../lib/components/startup-progress"
10
10
  import { ensureUnambiguousCompanion, launchAmbiguityDeps } from "../shared/companion-selection";
11
11
  import { runIndexCapCommand } from "../cap/index-cmd";
12
12
  import type { LaunchTarget } from "../../launch-selector";
13
+ import { writeJsonStdout } from "../../write-json-stdout";
13
14
 
14
15
  const STEP_LABELS = {
15
16
  sync: "Syncing mate",
@@ -127,7 +128,7 @@ export async function runLaunchToolCommand(
127
128
  * The printed result stays useful, but the status is what a supervisor
128
129
  * reads: a failed agent must fail the launch.
129
130
  */
130
- console.log(JSON.stringify(result, null, 2));
131
+ await writeJsonStdout(result);
131
132
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
132
133
  } catch (error) {
133
134
  progress?.failCurrent();
@@ -4,10 +4,7 @@ import githubLight from "@tanstack/highlight/themes/github-light";
4
4
 
5
5
  /** Sole boundary against `@tanstack/highlight`, so an upstream API break stays a single-file fix. */
6
6
 
7
- /** Language every unknown or unsupported candidate resolves to. */
8
- export const PLAIN_LANGUAGE = "plaintext";
9
-
10
- export function resolveHighlightLanguage(candidate: string | undefined): string {
7
+ function resolveHighlightLanguage(candidate: string | undefined): string {
11
8
  return normalizeLanguage(candidate);
12
9
  }
13
10
 
@@ -456,5 +456,3 @@ const VISUAL_SECTION_CSS = `
456
456
  }`;
457
457
 
458
458
  type ReportMetricsSection = Extract<ReportSection, { type: "metrics" }>;
459
-
460
- export { escapeHTML };
@@ -0,0 +1,203 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+
3
+ /** Shortest operator-supplied token accepted; a generated one is 43 characters. */
4
+ export const MIN_TOKEN_LENGTH = 32;
5
+
6
+ const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
7
+
8
+ export type StudioInvocation = "interactive" | "serve";
9
+
10
+ export interface StudioAccessOptions {
11
+ invocation: StudioInvocation;
12
+ terminal: boolean;
13
+ /** Bound interface as passed to the server. */
14
+ hostname: string;
15
+ /** Read at request time: the port is known only once the server has bound. */
16
+ port: () => number;
17
+ allowedHosts?: readonly string[];
18
+ publicOrigin?: string | null;
19
+ token?: string | null;
20
+ }
21
+
22
+ export type StudioRequirement = "none" | "token";
23
+
24
+ /**
25
+ * One server's Host, Origin, and token policy. `null` token means the server
26
+ * is unguarded and issues none.
27
+ */
28
+ export interface StudioAccess {
29
+ invocation: StudioInvocation;
30
+ guarded: boolean;
31
+ token: string | null;
32
+ cookieName(): string;
33
+ /** Accepted origin for this request, or `null` when its Host is not one Studio answers to. */
34
+ acceptedOrigin(request: Request): string | null;
35
+ originMatches(request: Request, acceptedOrigin: string): boolean;
36
+ hasToken(request: Request): boolean;
37
+ /** Whether a request of this kind needs the token under this invocation. */
38
+ requires(kind: "read" | "save" | "terminal"): boolean;
39
+ }
40
+
41
+ export function generateStudioToken(): string {
42
+ return randomBytes(32).toString("base64url");
43
+ }
44
+
45
+ export function validateStudioToken(value: string): string | null {
46
+ if (value.trim().length < MIN_TOKEN_LENGTH) {
47
+ return `the supplied token must be at least ${MIN_TOKEN_LENGTH} characters`;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** `null` for anything other than an exact `http(s)://host[:port]` origin. */
53
+ export function normalizeOrigin(value: string): string | null {
54
+ let parsed: URL;
55
+ try {
56
+ parsed = new URL(value);
57
+ } catch {
58
+ return null;
59
+ }
60
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
61
+ if (parsed.username || parsed.password) return null;
62
+ if (parsed.pathname !== "/" || parsed.search || parsed.hash || value.endsWith("/")) return null;
63
+ return parsed.origin;
64
+ }
65
+
66
+ /** Lower-cased `host[:port]` with the HTTP default port dropped, or `null` when unparsable. */
67
+ export function normalizeAuthority(value: string): string | null {
68
+ const trimmed = value.trim();
69
+ if (!trimmed || /[\s/?#@\\]/.test(trimmed)) return null;
70
+ try {
71
+ const parsed = new URL(`http://${trimmed}`);
72
+ return parsed.host;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function bracketed(hostname: string): string {
79
+ return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
80
+ }
81
+
82
+ function digest(value: string): Buffer {
83
+ return createHash("sha256").update(value).digest();
84
+ }
85
+
86
+ /** Hashing first keeps the comparison constant-time for inputs of any length. */
87
+ export function tokensEqual(candidate: string, expected: string): boolean {
88
+ return timingSafeEqual(digest(candidate), digest(expected));
89
+ }
90
+
91
+ function readCookie(request: Request, name: string): string | null {
92
+ const header = request.headers.get("cookie");
93
+ if (!header) return null;
94
+ for (const part of header.split(";")) {
95
+ const index = part.indexOf("=");
96
+ if (index < 0) continue;
97
+ if (part.slice(0, index).trim() === name) return part.slice(index + 1).trim();
98
+ }
99
+ return null;
100
+ }
101
+
102
+ export function createStudioAccess(options: StudioAccessOptions): StudioAccess {
103
+ const guarded = options.invocation === "serve" || options.terminal;
104
+ const token = guarded ? (options.token ?? generateStudioToken()) : null;
105
+ const publicOrigin = options.publicOrigin ? normalizeOrigin(options.publicOrigin) : null;
106
+ const publicAuthority = publicOrigin ? new URL(publicOrigin).host : null;
107
+ const configured = new Set<string>();
108
+ for (const host of options.allowedHosts ?? []) {
109
+ const authority = normalizeAuthority(host);
110
+ if (authority) configured.add(authority);
111
+ }
112
+ if (publicAuthority) configured.add(publicAuthority);
113
+
114
+ const accepted = (): Set<string> => {
115
+ const port = options.port();
116
+ const hosts = new Set(configured);
117
+ for (const hostname of [...LOOPBACK_HOSTNAMES, bracketed(options.hostname)]) {
118
+ const authority = normalizeAuthority(`${hostname}:${port}`);
119
+ if (authority) hosts.add(authority);
120
+ }
121
+ return hosts;
122
+ };
123
+
124
+ return {
125
+ invocation: options.invocation,
126
+ guarded,
127
+ token,
128
+ cookieName: () => `mate_studio_${options.port()}`,
129
+ acceptedOrigin(request) {
130
+ const raw = request.headers.get("host") ?? new URL(request.url).host;
131
+ const authority = normalizeAuthority(raw);
132
+ if (!authority || !accepted().has(authority)) return null;
133
+ if (publicOrigin && authority === publicAuthority) return publicOrigin;
134
+ return `http://${authority}`;
135
+ },
136
+ originMatches(request, acceptedOrigin) {
137
+ const origin = request.headers.get("origin");
138
+ if (!origin || origin === "null") return false;
139
+ return normalizeOrigin(origin) === acceptedOrigin;
140
+ },
141
+ hasToken(request) {
142
+ if (!token) return false;
143
+ const value = readCookie(request, `mate_studio_${options.port()}`);
144
+ return value !== null && tokensEqual(value, token);
145
+ },
146
+ requires(kind) {
147
+ if (!guarded) return false;
148
+ if (options.invocation === "serve") return true;
149
+ return kind !== "read";
150
+ },
151
+ };
152
+ }
153
+
154
+ const EXCHANGE_HEADERS = {
155
+ "cache-control": "no-store",
156
+ "referrer-policy": "no-referrer",
157
+ };
158
+
159
+ /**
160
+ * Trades a `?token=` navigation for the cookie and a relative redirect, so the
161
+ * browser never leaves the Host it asked for. `null` when the request carries
162
+ * no token parameter.
163
+ */
164
+ export function exchangeStudioToken(
165
+ request: Request,
166
+ access: StudioAccess,
167
+ acceptedOrigin: string,
168
+ ): Response | null {
169
+ const url = new URL(request.url);
170
+ if (!url.searchParams.has("token")) return null;
171
+ if (request.method !== "GET" || !access.token) {
172
+ return new Response("the access token is exchanged only by opening its address", {
173
+ status: 400,
174
+ headers: EXCHANGE_HEADERS,
175
+ });
176
+ }
177
+ const candidate = url.searchParams.get("token") ?? "";
178
+ if (!tokensEqual(candidate, access.token)) {
179
+ return new Response("the access token does not match; open the address Studio printed", {
180
+ status: 403,
181
+ headers: EXCHANGE_HEADERS,
182
+ });
183
+ }
184
+ url.searchParams.delete("token");
185
+ const secure = acceptedOrigin.startsWith("https:") ? "; Secure" : "";
186
+ return new Response(null, {
187
+ status: 303,
188
+ headers: {
189
+ ...EXCHANGE_HEADERS,
190
+ location: `${url.pathname}${url.search}`,
191
+ "set-cookie": `${access.cookieName()}=${access.token}; Path=/; HttpOnly; SameSite=Strict${secure}`,
192
+ },
193
+ });
194
+ }
195
+
196
+ /** Address the operator opens: loopback for wildcard or loopback binds, carrying the token only when it was generated. */
197
+ export function studioAddress(hostname: string, port: number, token: string | null): string {
198
+ const wildcard = hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]";
199
+ const loopback = ["127.0.0.1", "localhost", "::1", "[::1]"].includes(hostname);
200
+ const host = wildcard || loopback ? "localhost" : bracketed(hostname);
201
+ const base = `http://${host}:${port}`;
202
+ return token ? `${base}/?token=${token}` : base;
203
+ }