@ocis/myagent-cli 0.2.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 (91) hide show
  1. package/README.md +357 -0
  2. package/dist/agent/context.d.ts +33 -0
  3. package/dist/agent/context.js +169 -0
  4. package/dist/agent/modes.d.ts +21 -0
  5. package/dist/agent/modes.js +84 -0
  6. package/dist/agent/prompt-builder.d.ts +9 -0
  7. package/dist/agent/prompt-builder.js +31 -0
  8. package/dist/agent/sessions.d.ts +38 -0
  9. package/dist/agent/sessions.js +130 -0
  10. package/dist/agent/todo.d.ts +18 -0
  11. package/dist/agent/todo.js +61 -0
  12. package/dist/agent/turn.d.ts +309 -0
  13. package/dist/agent/turn.js +1253 -0
  14. package/dist/approval/policy.d.ts +81 -0
  15. package/dist/approval/policy.js +157 -0
  16. package/dist/config.d.ts +49 -0
  17. package/dist/config.js +156 -0
  18. package/dist/git/status.d.ts +89 -0
  19. package/dist/git/status.js +226 -0
  20. package/dist/headless.d.ts +72 -0
  21. package/dist/headless.js +330 -0
  22. package/dist/index.d.ts +60 -0
  23. package/dist/index.js +511 -0
  24. package/dist/protocol/client.d.ts +123 -0
  25. package/dist/protocol/client.js +250 -0
  26. package/dist/protocol/sse-frames.d.ts +6 -0
  27. package/dist/protocol/sse-frames.js +75 -0
  28. package/dist/protocol/types.d.ts +200 -0
  29. package/dist/protocol/types.js +8 -0
  30. package/dist/runtime.d.ts +38 -0
  31. package/dist/runtime.js +166 -0
  32. package/dist/sanitize.d.ts +1 -0
  33. package/dist/sanitize.js +21 -0
  34. package/dist/skills/discovery.d.ts +24 -0
  35. package/dist/skills/discovery.js +109 -0
  36. package/dist/tools/binary.d.ts +2 -0
  37. package/dist/tools/binary.js +22 -0
  38. package/dist/tools/diff.d.ts +1 -0
  39. package/dist/tools/diff.js +49 -0
  40. package/dist/tools/find.d.ts +2 -0
  41. package/dist/tools/find.js +61 -0
  42. package/dist/tools/fs.d.ts +2 -0
  43. package/dist/tools/fs.js +276 -0
  44. package/dist/tools/glob.d.ts +6 -0
  45. package/dist/tools/glob.js +131 -0
  46. package/dist/tools/grep.d.ts +3 -0
  47. package/dist/tools/grep.js +228 -0
  48. package/dist/tools/paths.d.ts +27 -0
  49. package/dist/tools/paths.js +124 -0
  50. package/dist/tools/registry.d.ts +13 -0
  51. package/dist/tools/registry.js +38 -0
  52. package/dist/tools/shell.d.ts +2 -0
  53. package/dist/tools/shell.js +136 -0
  54. package/dist/tools/skills.d.ts +2 -0
  55. package/dist/tools/skills.js +36 -0
  56. package/dist/tools/todo.d.ts +2 -0
  57. package/dist/tools/todo.js +43 -0
  58. package/dist/tools/transfer.d.ts +2 -0
  59. package/dist/tools/transfer.js +145 -0
  60. package/dist/tools/truncate.d.ts +12 -0
  61. package/dist/tools/truncate.js +46 -0
  62. package/dist/tools/types.d.ts +85 -0
  63. package/dist/tools/types.js +63 -0
  64. package/dist/ui/app.d.ts +39 -0
  65. package/dist/ui/app.js +1061 -0
  66. package/dist/ui/colors.d.ts +100 -0
  67. package/dist/ui/colors.js +169 -0
  68. package/dist/ui/components.d.ts +267 -0
  69. package/dist/ui/components.js +811 -0
  70. package/dist/ui/diff.d.ts +37 -0
  71. package/dist/ui/diff.js +143 -0
  72. package/dist/ui/format.d.ts +28 -0
  73. package/dist/ui/format.js +76 -0
  74. package/dist/ui/help.d.ts +6 -0
  75. package/dist/ui/help.js +45 -0
  76. package/dist/ui/highlight.d.ts +20 -0
  77. package/dist/ui/highlight.js +210 -0
  78. package/dist/ui/logo.d.ts +24 -0
  79. package/dist/ui/logo.js +106 -0
  80. package/dist/ui/model-list.d.ts +10 -0
  81. package/dist/ui/model-list.js +33 -0
  82. package/dist/ui/quit-confirm.d.ts +8 -0
  83. package/dist/ui/quit-confirm.js +40 -0
  84. package/dist/ui/select-popup.d.ts +30 -0
  85. package/dist/ui/select-popup.js +54 -0
  86. package/dist/ui/theme.d.ts +4 -0
  87. package/dist/ui/theme.js +41 -0
  88. package/dist/ui/tool-view.d.ts +20 -0
  89. package/dist/ui/tool-view.js +326 -0
  90. package/package.json +44 -0
  91. package/skills/git-commit/SKILL.md +27 -0
@@ -0,0 +1,81 @@
1
+ import type { ApprovalRequest } from "../tools/types.js";
2
+ import type { AgentMode } from "../agent/modes.js";
3
+ export type ApprovalMode = "ask" | "auto";
4
+ export type ApprovalOutcome = "approved" | "denied" | "timeout" | "cancelled";
5
+ export interface ApprovalPrompt {
6
+ (request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalOutcome>;
7
+ }
8
+ /** Fixed approval deadline — never reset by keypresses. */
9
+ export declare const APPROVAL_TIMEOUT_MS: number;
10
+ export declare class ApprovalPolicy {
11
+ private mode;
12
+ private readonly prompt;
13
+ private readonly getAgentMode;
14
+ /**
15
+ * Text handed to the model when a mutating tool is denied. Headless mode
16
+ * overrides it: with no one to ask, "the user declined" would be false.
17
+ */
18
+ readonly deniedMessage: string;
19
+ /**
20
+ * Text handed to the model when the approval deadline passes. The agent
21
+ * must stop waiting on the user and say so instead of hanging.
22
+ */
23
+ readonly timeoutMessage: string;
24
+ /** Injectable for tests; the deadline is fixed everywhere else. */
25
+ private readonly timeoutMs;
26
+ /**
27
+ * Every approval not yet settled — the one being shown plus the ones queued
28
+ * behind it. Concurrency is real (a round's read-only calls run in parallel
29
+ * and outside-workspace reads prompt; a second tool round can start while an
30
+ * earlier one waits), so a single slot would lose an earlier prompt's cancel
31
+ * and leave it hanging until the deadline.
32
+ */
33
+ private pending;
34
+ /**
35
+ * Serializes the prompts themselves: the UI shows ONE approval card, so a
36
+ * second concurrent prompt would overwrite the first's resolver and strand
37
+ * it. Each waiter releases the next in its `finally`.
38
+ */
39
+ private queue;
40
+ /** Approvals in flight (prompting or queued) — 0 means the card is free. */
41
+ private inFlight;
42
+ constructor(mode: ApprovalMode, prompt: ApprovalPrompt, getAgentMode: () => AgentMode,
43
+ /**
44
+ * Text handed to the model when a mutating tool is denied. Headless mode
45
+ * overrides it: with no one to ask, "the user declined" would be false.
46
+ */
47
+ deniedMessage?: string,
48
+ /**
49
+ * Text handed to the model when the approval deadline passes. The agent
50
+ * must stop waiting on the user and say so instead of hanging.
51
+ */
52
+ timeoutMessage?: string,
53
+ /** Injectable for tests; the deadline is fixed everywhere else. */
54
+ timeoutMs?: number);
55
+ get current(): ApprovalMode;
56
+ setMode(mode: ApprovalMode): void;
57
+ toggle(): ApprovalMode;
58
+ /** Decide whether a tool may run. Outside-workspace calls always prompt. */
59
+ approve(request: ApprovalRequest): Promise<ApprovalOutcome>;
60
+ /**
61
+ * Cancel pending approvals — the one on screen and any queued behind it.
62
+ * With `toolCallIds`, only the ones about those calls (a release of another
63
+ * call in the same round must not cancel this one). Called by the runner
64
+ * when the server releases a call or the run is stopped — a released call
65
+ * must never run.
66
+ */
67
+ cancelPending(toolCallIds?: string[]): void;
68
+ /**
69
+ * Race the prompt against the fixed deadline. The deadline is enforced HERE
70
+ * (single source of truth); the signal lets the UI dismiss its card when
71
+ * the deadline passes or the approval is cancelled. The deadline promise
72
+ * settles before the abort fires, so a prompt that also resolves on abort
73
+ * can never beat the policy's outcome.
74
+ *
75
+ * The deadline runs from THIS call, not from the moment the card appears:
76
+ * a queued approval that waits out the deadline behind another one settles
77
+ * as `timeout` without ever prompting — the agent must not be left waiting
78
+ * on a card the user will never see.
79
+ */
80
+ private promptWithDeadline;
81
+ }
@@ -0,0 +1,157 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Approval policy.
3
+ //
4
+ // `auto` (the default) runs workspace work unattended, but still prompts when a
5
+ // call reaches outside the workspace. `ask` prompts before every mutating tool
6
+ // (and for outside reads too). The mode is per session, toggled with /edit or
7
+ // Shift+Tab; read-only workspace tools never prompt. Plan mode is enforced at
8
+ // execution time in AgentRunner (mutating tools refuse); this layer's plan
9
+ // check is the second line of defense in case a call ever reaches approval.
10
+ //
11
+ // The prompt resolves to a 4-state outcome rather than a boolean: `timeout`
12
+ // (the fixed 10-minute deadline passed — the agent gets an error result so it
13
+ // stops waiting) and `cancelled` (the server released the call, or the run was
14
+ // stopped — the tool never runs and nothing is submitted) must be
15
+ // distinguishable from a plain deny. Both the deadline and the cancellation are
16
+ // enforced HERE: every prompt implementation gets them for free, and its
17
+ // AbortSignal is the dismissal hook for whatever UI is showing.
18
+ // ---------------------------------------------------------------------------
19
+ import { isReadOnlyTool } from "../agent/modes.js";
20
+ /** Fixed approval deadline — never reset by keypresses. */
21
+ export const APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
22
+ export class ApprovalPolicy {
23
+ mode;
24
+ prompt;
25
+ getAgentMode;
26
+ deniedMessage;
27
+ timeoutMessage;
28
+ timeoutMs;
29
+ /**
30
+ * Every approval not yet settled — the one being shown plus the ones queued
31
+ * behind it. Concurrency is real (a round's read-only calls run in parallel
32
+ * and outside-workspace reads prompt; a second tool round can start while an
33
+ * earlier one waits), so a single slot would lose an earlier prompt's cancel
34
+ * and leave it hanging until the deadline.
35
+ */
36
+ pending = new Set();
37
+ /**
38
+ * Serializes the prompts themselves: the UI shows ONE approval card, so a
39
+ * second concurrent prompt would overwrite the first's resolver and strand
40
+ * it. Each waiter releases the next in its `finally`.
41
+ */
42
+ queue = Promise.resolve();
43
+ /** Approvals in flight (prompting or queued) — 0 means the card is free. */
44
+ inFlight = 0;
45
+ constructor(mode, prompt, getAgentMode,
46
+ /**
47
+ * Text handed to the model when a mutating tool is denied. Headless mode
48
+ * overrides it: with no one to ask, "the user declined" would be false.
49
+ */
50
+ deniedMessage = "The user declined to run this action.",
51
+ /**
52
+ * Text handed to the model when the approval deadline passes. The agent
53
+ * must stop waiting on the user and say so instead of hanging.
54
+ */
55
+ timeoutMessage = "Error: request approval timeout — the user did not respond within 10 minutes. Stop and wait for the user.",
56
+ /** Injectable for tests; the deadline is fixed everywhere else. */
57
+ timeoutMs = APPROVAL_TIMEOUT_MS) {
58
+ this.mode = mode;
59
+ this.prompt = prompt;
60
+ this.getAgentMode = getAgentMode;
61
+ this.deniedMessage = deniedMessage;
62
+ this.timeoutMessage = timeoutMessage;
63
+ this.timeoutMs = timeoutMs;
64
+ }
65
+ get current() {
66
+ return this.mode;
67
+ }
68
+ setMode(mode) {
69
+ this.mode = mode;
70
+ }
71
+ toggle() {
72
+ this.mode = this.mode === "auto" ? "ask" : "auto";
73
+ return this.mode;
74
+ }
75
+ /** Decide whether a tool may run. Outside-workspace calls always prompt. */
76
+ async approve(request) {
77
+ // Plan mode keys off the per-CALL mutation verdict when the caller has it
78
+ // (a tool whose arguments decide it — transfer get vs put/list); the
79
+ // name-based read-only list is the fallback for callers that don't.
80
+ const sideEffecting = request.mutating ?? !isReadOnlyTool(request.tool);
81
+ if (sideEffecting && this.getAgentMode() === "plan") {
82
+ return "denied";
83
+ }
84
+ if (request.outsideWorkspace)
85
+ return this.promptWithDeadline(request);
86
+ if (!sideEffecting || this.mode === "auto")
87
+ return "approved";
88
+ return this.promptWithDeadline(request);
89
+ }
90
+ /**
91
+ * Cancel pending approvals — the one on screen and any queued behind it.
92
+ * With `toolCallIds`, only the ones about those calls (a release of another
93
+ * call in the same round must not cancel this one). Called by the runner
94
+ * when the server releases a call or the run is stopped — a released call
95
+ * must never run.
96
+ */
97
+ cancelPending(toolCallIds) {
98
+ for (const pending of [...this.pending]) {
99
+ // With a filter, only entries about those calls cancel. An entry with
100
+ // no callId cannot be matched, so a release of another call must leave
101
+ // it alone (the unfiltered form cancels everything).
102
+ if (toolCallIds && (pending.callId === undefined || !toolCallIds.includes(pending.callId)))
103
+ continue;
104
+ pending.cancel();
105
+ }
106
+ }
107
+ /**
108
+ * Race the prompt against the fixed deadline. The deadline is enforced HERE
109
+ * (single source of truth); the signal lets the UI dismiss its card when
110
+ * the deadline passes or the approval is cancelled. The deadline promise
111
+ * settles before the abort fires, so a prompt that also resolves on abort
112
+ * can never beat the policy's outcome.
113
+ *
114
+ * The deadline runs from THIS call, not from the moment the card appears:
115
+ * a queued approval that waits out the deadline behind another one settles
116
+ * as `timeout` without ever prompting — the agent must not be left waiting
117
+ * on a card the user will never see.
118
+ */
119
+ async promptWithDeadline(request) {
120
+ const controller = new AbortController();
121
+ const deadline = Promise.withResolvers();
122
+ let settled = false;
123
+ const finish = (outcome) => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ deadline.resolve(outcome);
128
+ controller.abort();
129
+ };
130
+ const timer = setTimeout(() => finish("timeout"), this.timeoutMs);
131
+ timer.unref?.();
132
+ const entry = { callId: request.callId, cancel: () => finish("cancelled") };
133
+ this.pending.add(entry);
134
+ // Take a slot in the prompt queue before awaiting the previous one, so
135
+ // arrival order is preserved. A free card (nothing in flight) is shown
136
+ // WITHOUT awaiting: the common single-approval case must not cost a tick.
137
+ const previous = this.queue;
138
+ const released = Promise.withResolvers();
139
+ const wait = this.inFlight > 0;
140
+ this.inFlight++;
141
+ this.queue = released.promise;
142
+ try {
143
+ // A cancel/timeout while queued settles immediately — no card, no wait.
144
+ if (wait)
145
+ await Promise.race([previous.catch(() => { }), deadline.promise]);
146
+ if (settled)
147
+ return await deadline.promise;
148
+ return await Promise.race([this.prompt(request, controller.signal), deadline.promise]);
149
+ }
150
+ finally {
151
+ this.pending.delete(entry);
152
+ this.inFlight--;
153
+ clearTimeout(timer);
154
+ released.resolve();
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,49 @@
1
+ export type ApprovalMode = "ask" | "auto";
2
+ export interface StoredConfig {
3
+ /** MyAgent backend base URL, e.g. https://agent.example.com */
4
+ baseUrl: string;
5
+ /** Integration ID from the MyAgent Integrations page. */
6
+ integrationId: string;
7
+ /** Integration API key (iak_...). */
8
+ apiKey: string;
9
+ /** Default approval mode for new sessions ("auto" = run tools unattended). */
10
+ approvalDefault: ApprovalMode;
11
+ /** @deprecated superseded by approvalDefault; kept for config compatibility. */
12
+ autoApprove?: boolean;
13
+ }
14
+ export declare const EMPTY_CONFIG: StoredConfig;
15
+ export declare function configDir(): string;
16
+ export declare function configPath(): string;
17
+ /** Local session index (cwd → known threads). Not secret; 0644. */
18
+ export declare function sessionsPath(): string;
19
+ /** Global context file, loaded before project AGENTS.md/CLAUDE.md files. */
20
+ export declare function globalContextPath(): string;
21
+ /**
22
+ * One-time migration: if the config dir from a previous name (myagent-cli)
23
+ * exists and the current one does not, rename it so existing credentials and
24
+ * skills carry over. Idempotent and best-effort.
25
+ */
26
+ export declare function migrateLegacyConfigDir(): Promise<void>;
27
+ export declare function loadConfig(): Promise<StoredConfig>;
28
+ export declare function saveConfig(config: StoredConfig): Promise<void>;
29
+ /**
30
+ * Resolve effective credentials from file config, environment, and flags.
31
+ * Later sources win. Returns the merged config plus whether it's usable.
32
+ */
33
+ export declare function resolveConfig(stored: StoredConfig, flags: Partial<StoredConfig>): StoredConfig;
34
+ export declare function isConfigured(config: StoredConfig): boolean;
35
+ /**
36
+ * Split a combined API base URL (`<host>/integrations/<id>`) into its parts.
37
+ * The Integrations page shows this single URL, so pasting it anywhere a base
38
+ * URL is accepted (wizard, --base-url, MYAGENT_BASE_URL, config file) must
39
+ * work. A trailing path is tolerated (old clipboards carry full endpoint URLs
40
+ * like `.../integrations/<id>/v1/chat/completions` — same rule as the
41
+ * extension's parseEndpointUrl); a URL without `/integrations/<id>` is
42
+ * returned unchanged.
43
+ */
44
+ export declare function splitIntegrationUrl(url: string): {
45
+ baseUrl: string;
46
+ integrationId?: string;
47
+ };
48
+ /** The combined API base URL shown in the UI, for display/copy. */
49
+ export declare function combinedIntegrationUrl(config: StoredConfig): string;
package/dist/config.js ADDED
@@ -0,0 +1,156 @@
1
+ // -------------------------------------------------------------------
2
+ // Persistent CLI configuration (credentials + preferences).
3
+ //
4
+ // Stored at $XDG_CONFIG_HOME/myagent/config.json (defaults to
5
+ // ~/.config/myagent/config.json), with 0600 perms since it holds an
6
+ // API key. Runtime values resolve in order: CLI flags > env vars > file.
7
+ // -------------------------------------------------------------------
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { mkdir, chmod, rename, stat } from "node:fs/promises";
11
+ import { fileExists, readJsonFile, writeFile } from "./runtime.js";
12
+ export const EMPTY_CONFIG = {
13
+ baseUrl: "",
14
+ integrationId: "",
15
+ apiKey: "",
16
+ approvalDefault: "auto",
17
+ };
18
+ function configBase() {
19
+ return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
20
+ }
21
+ export function configDir() {
22
+ return join(configBase(), "myagent");
23
+ }
24
+ export function configPath() {
25
+ return join(configDir(), "config.json");
26
+ }
27
+ /** Local session index (cwd → known threads). Not secret; 0644. */
28
+ export function sessionsPath() {
29
+ return join(configDir(), "sessions.json");
30
+ }
31
+ /** Global context file, loaded before project AGENTS.md/CLAUDE.md files. */
32
+ export function globalContextPath() {
33
+ return join(configDir(), "AGENTS.md");
34
+ }
35
+ /**
36
+ * One-time migration: if the config dir from a previous name (myagent-cli)
37
+ * exists and the current one does not, rename it so existing credentials and
38
+ * skills carry over. Idempotent and best-effort.
39
+ */
40
+ export async function migrateLegacyConfigDir() {
41
+ const current = configDir();
42
+ const legacy = join(configBase(), "myagent-cli");
43
+ if (current === legacy)
44
+ return;
45
+ try {
46
+ const hasCurrent = await stat(current).then(() => true).catch(() => false);
47
+ const hasLegacy = await stat(legacy).then(() => true).catch(() => false);
48
+ if (!hasCurrent && hasLegacy)
49
+ await rename(legacy, current);
50
+ }
51
+ catch {
52
+ /* best-effort — a failed migration just means the user reconfigures */
53
+ }
54
+ }
55
+ export async function loadConfig() {
56
+ try {
57
+ const path = configPath();
58
+ if (!(await fileExists(path)))
59
+ return { ...EMPTY_CONFIG };
60
+ const data = await readJsonFile(path);
61
+ const merged = { ...EMPTY_CONFIG, ...data };
62
+ // Legacy field: autoApprove:false used to mean "ask" — honour it once.
63
+ if (data.approvalDefault === undefined && data.autoApprove === false) {
64
+ merged.approvalDefault = "ask";
65
+ }
66
+ // Fail CLOSED: only an explicit "auto" means auto — corrupted files,
67
+ // typos, or future unknown values must fall back to asking, never to
68
+ // unattended tool execution.
69
+ merged.approvalDefault = merged.approvalDefault === "auto" ? "auto" : "ask";
70
+ // A stored combined URL (`<host>/integrations/<id>`) is self-contained —
71
+ // its id is authoritative for this source.
72
+ applyBaseUrl(merged, merged.baseUrl);
73
+ return merged;
74
+ }
75
+ catch {
76
+ // Corrupt or unreadable config — start from empty rather than crashing.
77
+ return { ...EMPTY_CONFIG };
78
+ }
79
+ }
80
+ export async function saveConfig(config) {
81
+ await mkdir(configDir(), { recursive: true });
82
+ // Write to a temp file then rename atomically so a mid-write crash or signal
83
+ // never leaves a truncated config (and the API key) behind.
84
+ const tmp = configPath() + ".tmp";
85
+ await writeFile(tmp, JSON.stringify(config, null, 2) + "\n");
86
+ // The file holds a secret — keep it owner-only. Best-effort on Windows.
87
+ try {
88
+ await chmod(tmp, 0o600);
89
+ }
90
+ catch {
91
+ /* chmod is a no-op / unsupported on some platforms */
92
+ }
93
+ await rename(tmp, configPath());
94
+ }
95
+ /**
96
+ * Resolve effective credentials from file config, environment, and flags.
97
+ * Later sources win. Returns the merged config plus whether it's usable.
98
+ */
99
+ export function resolveConfig(stored, flags) {
100
+ const env = {};
101
+ if (process.env.MYAGENT_BASE_URL)
102
+ applyBaseUrl(env, process.env.MYAGENT_BASE_URL);
103
+ if (process.env.MYAGENT_INTEGRATION_ID)
104
+ env.integrationId = process.env.MYAGENT_INTEGRATION_ID;
105
+ if (process.env.MYAGENT_API_KEY)
106
+ env.apiKey = process.env.MYAGENT_API_KEY;
107
+ if (process.env.MYAGENT_APPROVAL === "ask")
108
+ env.approvalDefault = "ask";
109
+ const merged = { ...stored, ...env };
110
+ // Only override with flags that were actually provided (non-empty).
111
+ if (flags.baseUrl)
112
+ applyBaseUrl(merged, flags.baseUrl);
113
+ if (flags.integrationId)
114
+ merged.integrationId = flags.integrationId;
115
+ if (flags.apiKey)
116
+ merged.apiKey = flags.apiKey;
117
+ if (flags.approvalDefault)
118
+ merged.approvalDefault = flags.approvalDefault;
119
+ return merged;
120
+ }
121
+ /**
122
+ * Set a base URL on a config patch, deriving the integration id when the URL
123
+ * is the combined form (`<host>/integrations/<id>`) — a combined URL is
124
+ * self-contained, so its id is authoritative for that source.
125
+ */
126
+ function applyBaseUrl(target, baseUrl) {
127
+ const split = splitIntegrationUrl(baseUrl);
128
+ target.baseUrl = split.baseUrl;
129
+ if (split.integrationId)
130
+ target.integrationId = split.integrationId;
131
+ }
132
+ export function isConfigured(config) {
133
+ return Boolean(config.baseUrl && config.integrationId && config.apiKey);
134
+ }
135
+ /**
136
+ * Split a combined API base URL (`<host>/integrations/<id>`) into its parts.
137
+ * The Integrations page shows this single URL, so pasting it anywhere a base
138
+ * URL is accepted (wizard, --base-url, MYAGENT_BASE_URL, config file) must
139
+ * work. A trailing path is tolerated (old clipboards carry full endpoint URLs
140
+ * like `.../integrations/<id>/v1/chat/completions` — same rule as the
141
+ * extension's parseEndpointUrl); a URL without `/integrations/<id>` is
142
+ * returned unchanged.
143
+ */
144
+ export function splitIntegrationUrl(url) {
145
+ const trimmed = url.trim();
146
+ const match = /^(.*?)\/integrations\/([^/]+)(?:\/.*)?$/.exec(trimmed);
147
+ if (!match)
148
+ return { baseUrl: trimmed };
149
+ return { baseUrl: match[1], integrationId: match[2] };
150
+ }
151
+ /** The combined API base URL shown in the UI, for display/copy. */
152
+ export function combinedIntegrationUrl(config) {
153
+ if (!config.baseUrl || !config.integrationId)
154
+ return config.baseUrl || "";
155
+ return `${config.baseUrl.replace(/\/+$/, "")}/integrations/${config.integrationId}`;
156
+ }
@@ -0,0 +1,89 @@
1
+ export interface GitFileChange {
2
+ path: string;
3
+ /** Porcelain status code (M, A, D, R, ??, …). */
4
+ status: string;
5
+ insertions: number;
6
+ deletions: number;
7
+ }
8
+ export interface GitStatus {
9
+ isRepo: boolean;
10
+ branch: string | null;
11
+ files: GitFileChange[];
12
+ totalInsertions: number;
13
+ totalDeletions: number;
14
+ }
15
+ export declare const EMPTY_GIT_STATUS: GitStatus;
16
+ /**
17
+ * Strip terminal-control sequences from a git-derived string (file paths,
18
+ * branch names). A malicious repository can carry filenames embedding ANSI/OSC
19
+ * escape sequences; rendering those raw would inject cursor moves, terminal
20
+ * title changes, or clipboard writes into the user's terminal. Applied at
21
+ * the parse boundary so every consumer (sidebar, footer, diff popup) is
22
+ * covered without each one having to remember.
23
+ */
24
+ import { sanitizeForTerminal } from "../sanitize.js";
25
+ export { sanitizeForTerminal };
26
+ /**
27
+ * Parse `git status --porcelain=v1 -z --branch` output. The -z form is
28
+ * NUL-separated and never quotes paths, so filenames with spaces, quotes,
29
+ * tabs, or non-ASCII bytes arrive verbatim — no unquoting step to get wrong.
30
+ * Renames are `XY PATH\0ORIG_PATH\0` (PATH is already the destination).
31
+ */
32
+ export declare function parsePorcelain(output: string): {
33
+ branch: string | null;
34
+ files: Array<{
35
+ path: string;
36
+ status: string;
37
+ }>;
38
+ };
39
+ /**
40
+ * Parse `git diff --numstat -z` output into a path → line-count map.
41
+ *
42
+ * The -z form never quotes or abbreviates: a regular record is
43
+ * `ins\tdel\tpath\0`; a rename is `ins\tdel\t\0ORIG\0DEST\0` (the stats field
44
+ * ends at the tab, then the two paths follow as their own NUL fields, orig
45
+ * first). Keying on DEST matches porcelain -z's destination path, so the
46
+ * panel's +/- counts line up with the file list for every filename — CJK,
47
+ * spaces, quotes — instead of silently zeroing on a quoted/abbreviated key.
48
+ */
49
+ export declare function parseNumstat(output: string): Map<string, {
50
+ insertions: number;
51
+ deletions: number;
52
+ }>;
53
+ /** Combine porcelain + staged/unstaged numstat into the panel model. */
54
+ export declare function mergeGitStatus(porcelain: {
55
+ branch: string | null;
56
+ files: Array<{
57
+ path: string;
58
+ status: string;
59
+ }>;
60
+ }, unstaged: Map<string, {
61
+ insertions: number;
62
+ deletions: number;
63
+ }>, staged: Map<string, {
64
+ insertions: number;
65
+ deletions: number;
66
+ }>): GitStatus;
67
+ /** Collect the current git status; never throws. */
68
+ export declare function collectGitStatus(workspace: string, signal?: AbortSignal): Promise<GitStatus>;
69
+ export interface FileDiff {
70
+ /** Unified diff text (possibly truncated). */
71
+ text: string;
72
+ truncated: boolean;
73
+ }
74
+ export declare const MAX_DIFF_LINES = 5000;
75
+ export declare const MAX_DIFF_CHARS: number;
76
+ /** Cut the diff to the char/line budget, reporting whether anything was lost. */
77
+ export declare function truncateDiff(text: string): FileDiff;
78
+ /**
79
+ * Unified diff for one working-tree file. Tracked files diff against HEAD
80
+ * (staged + unstaged); when there is no HEAD yet (fresh `git init`) the
81
+ * index/worktree pair is diffed instead, so staged edits still show.
82
+ * Untracked files diff against /dev/null, where git exits 1 even on success.
83
+ *
84
+ * Porcelain paths are relative to the repository root, so the diff runs from
85
+ * the root too — otherwise a workspace that is a subdirectory of the repo
86
+ * resolves the pathspec against the wrong prefix and produces nothing.
87
+ * Returns an empty text when git cannot produce a diff.
88
+ */
89
+ export declare function collectFileDiff(workspace: string, file: GitFileChange, signal?: AbortSignal): Promise<FileDiff>;