@robota-sdk/agent-command 3.0.0-beta.83 → 3.0.0-beta.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +71 -62
- package/dist/node/index.cjs +75 -72
- package/dist/node/index.d.cts +37 -45
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +37 -45
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +78 -75
- package/dist/node/index.js.map +1 -1
- package/package.json +9 -8
- package/src/agent/__tests__/agent-command.test.ts +62 -0
- package/src/agent/agent-command-parser.ts +2 -2
- package/src/agent/agent-command.ts +20 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +2 -2
- package/src/default/__tests__/default-command-modules.test.ts +5 -4
- package/src/default/__tests__/model-exposure.test.ts +2 -0
- package/src/default/default-command-modules.ts +8 -0
- package/src/effort/effort-command.test.ts +65 -2
- package/src/effort/effort-command.ts +19 -5
- package/src/git/__tests__/fake-git-port.ts +9 -2
- package/src/git/git-command-module.ts +3 -4
- package/src/git/git-commit.ts +6 -3
- package/src/git/index.ts +4 -1
- package/src/goal/goal-command.ts +3 -1
- package/src/index.ts +6 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +131 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +6 -0
- package/src/mcp-activation/mcp-activation-command.ts +54 -1
- package/src/memory/__tests__/memory-command-module.test.ts +40 -2
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/memory-command-module.ts +4 -2
- package/src/memory/memory-command.ts +42 -2
- package/src/mode/__tests__/mode-command-module.test.ts +40 -0
- package/src/mode/mode-command.ts +1 -1
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +240 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +223 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +81 -1
- package/src/plugin/plugin-command.ts +47 -4
- package/src/provider/__tests__/org-policy.test.ts +4 -0
- package/src/provider/__tests__/provider-command-module.test.ts +276 -14
- package/src/provider/__tests__/scripted-interaction.ts +12 -2
- package/src/provider/provider-command-execution.ts +54 -8
- package/src/provider/provider-command-profile-lifecycle.ts +48 -2
- package/src/provider/provider-command-profile-operations.ts +14 -0
- package/src/provider/provider-command-profile.ts +7 -4
- package/src/provider/provider-command-setup.ts +14 -1
- package/src/rewind/__tests__/rewind-command-module.test.ts +2 -1
- package/src/schedule/__tests__/loop-command-functional.test.ts +13 -0
- package/src/schedule/__tests__/loop-command.test.ts +238 -4
- package/src/schedule/loop-command.ts +95 -3
- package/src/schedule/schedule-command-module.ts +8 -4
- package/src/settings/settings-command-module.ts +2 -2
- package/src/git/__tests__/git-diff.test.ts +0 -125
- package/src/git/__tests__/git-process.test.ts +0 -221
- package/src/git/__tests__/git-status.test.ts +0 -117
- package/src/git/git-diff.ts +0 -129
- package/src/git/git-process.ts +0 -169
- package/src/git/git-status.ts +0 -134
package/src/git/git-process.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BEHAVIOR-2437: the one seam every `/git` verb runs git through.
|
|
3
|
-
*
|
|
4
|
-
* argv only — `execFile` with `shell: false`, so no user token is ever parsed as shell syntax; stdin
|
|
5
|
-
* is closed at once, so a hook that reads it sees EOF instead of hanging; output is capped, the child
|
|
6
|
-
* is bounded by a timeout, and every way a run can end is a typed outcome rather than a throw. Exit
|
|
7
|
-
* codes are DATA: `diff --cached --quiet` answers with 1, and that is an answer, not an error.
|
|
8
|
-
*/
|
|
9
|
-
import { execFile } from 'node:child_process';
|
|
10
|
-
import { constants } from 'node:os';
|
|
11
|
-
|
|
12
|
-
import type { ExecFileException } from 'node:child_process';
|
|
13
|
-
|
|
14
|
-
export const GIT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
15
|
-
const BYTES_PER_KIB = 1024;
|
|
16
|
-
const OUTPUT_CAP_MIB = 16;
|
|
17
|
-
export const GIT_MAX_OUTPUT_BYTES = OUTPUT_CAP_MIB * BYTES_PER_KIB * BYTES_PER_KIB;
|
|
18
|
-
/** The shell convention for a signal-terminated child: 128 + the signal number. */
|
|
19
|
-
const SIGNAL_EXIT_BASE = 128;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Variables that redirect a git child to ANOTHER repository. Git exports them into hooks, so a
|
|
23
|
-
* `/git` run started from inside a hook (or from any process that inherited one) would otherwise
|
|
24
|
-
* operate on the wrong tree. Identity and configuration variables are deliberately NOT here — the
|
|
25
|
-
* user's `GIT_CONFIG_GLOBAL`, `GIT_AUTHOR_*`, `GPG_TTY`, `SSH_AUTH_SOCK` … are what make the commit
|
|
26
|
-
* theirs.
|
|
27
|
-
*/
|
|
28
|
-
export const GIT_ENV_DENYLIST: readonly string[] = [
|
|
29
|
-
'GIT_DIR',
|
|
30
|
-
'GIT_WORK_TREE',
|
|
31
|
-
'GIT_INDEX_FILE',
|
|
32
|
-
'GIT_OBJECT_DIRECTORY',
|
|
33
|
-
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
34
|
-
'GIT_COMMON_DIR',
|
|
35
|
-
'GIT_PREFIX',
|
|
36
|
-
'GIT_NAMESPACE',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
export interface IGitProcessRunOptions {
|
|
40
|
-
cwd: string;
|
|
41
|
-
timeoutMs?: number;
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* `not-found` covers every spawn failure — ENOENT, EACCES, ENOTDIR, … — with the code in `detail`;
|
|
47
|
-
* the other three are the run's own bounds.
|
|
48
|
-
*/
|
|
49
|
-
export type TGitProcessFailureReason = 'not-found' | 'timeout' | 'aborted' | 'output-too-large';
|
|
50
|
-
|
|
51
|
-
export type TGitProcessOutcome =
|
|
52
|
-
| { kind: 'exited'; stdout: string; stderr: string; exitCode: number }
|
|
53
|
-
| { kind: 'failed'; reason: TGitProcessFailureReason; detail: string };
|
|
54
|
-
|
|
55
|
-
export interface IGitProcessPort {
|
|
56
|
-
run(args: readonly string[], options: IGitProcessRunOptions): Promise<TGitProcessOutcome>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ICreateGitProcessOptions {
|
|
60
|
-
/** The executable to run; `git` on PATH by default. */
|
|
61
|
-
executable?: string;
|
|
62
|
-
/** The environment to derive the child's from; the live process environment by default. */
|
|
63
|
-
env?: NodeJS.ProcessEnv;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The child's environment: `source` minus the repository-redirecting variables. */
|
|
67
|
-
export function gitEnvironment(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
68
|
-
const env: NodeJS.ProcessEnv = {};
|
|
69
|
-
for (const [key, value] of Object.entries(source)) {
|
|
70
|
-
if (!GIT_ENV_DENYLIST.includes(key)) env[key] = value;
|
|
71
|
-
}
|
|
72
|
-
return env;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function failed(reason: TGitProcessFailureReason, detail: string): TGitProcessOutcome {
|
|
76
|
-
return { kind: 'failed', reason, detail };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function signalNumber(signal: NodeJS.Signals): number {
|
|
80
|
-
return constants.signals[signal] ?? 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function toOutcome(
|
|
84
|
-
error: ExecFileException | null,
|
|
85
|
-
stdout: string,
|
|
86
|
-
stderr: string,
|
|
87
|
-
context: { executable: string; timeoutMs: number; signal: AbortSignal | undefined },
|
|
88
|
-
): TGitProcessOutcome {
|
|
89
|
-
if (error === null) return { kind: 'exited', stdout, stderr, exitCode: 0 };
|
|
90
|
-
if (context.signal?.aborted === true || error.name === 'AbortError') {
|
|
91
|
-
return failed('aborted', 'the run was aborted before git exited');
|
|
92
|
-
}
|
|
93
|
-
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
94
|
-
return failed(
|
|
95
|
-
'output-too-large',
|
|
96
|
-
`git produced more than ${OUTPUT_CAP_MIB} MiB of output and was stopped`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
// Our own kill wins over whatever exit the child managed on the way down (a shell trap exiting
|
|
100
|
-
// 143 is still a timeout).
|
|
101
|
-
if (error.killed === true) {
|
|
102
|
-
return failed('timeout', `git did not exit within ${context.timeoutMs} ms and was stopped`);
|
|
103
|
-
}
|
|
104
|
-
if (typeof error.code === 'number')
|
|
105
|
-
return { kind: 'exited', stdout, stderr, exitCode: error.code };
|
|
106
|
-
if (error.signal) {
|
|
107
|
-
// RUNTIME-53 convention: a signal-terminated child reports 128 + the signal number.
|
|
108
|
-
return {
|
|
109
|
-
kind: 'exited',
|
|
110
|
-
stdout,
|
|
111
|
-
stderr,
|
|
112
|
-
exitCode: SIGNAL_EXIT_BASE + signalNumber(error.signal),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return failed(
|
|
116
|
-
'not-found',
|
|
117
|
-
`${context.executable} could not be run (${String(error.code ?? error.message)})`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** The production port: argv-only `execFile`, stdin closed, output capped, timeout bounded. */
|
|
122
|
-
export function createGitProcess(options: ICreateGitProcessOptions = {}): IGitProcessPort {
|
|
123
|
-
const executable = options.executable ?? 'git';
|
|
124
|
-
return {
|
|
125
|
-
run(args, runOptions) {
|
|
126
|
-
const timeoutMs = runOptions.timeoutMs ?? GIT_DEFAULT_TIMEOUT_MS;
|
|
127
|
-
const env = gitEnvironment(options.env ?? process.env);
|
|
128
|
-
return new Promise<TGitProcessOutcome>((resolve) => {
|
|
129
|
-
const child = execFile(
|
|
130
|
-
executable,
|
|
131
|
-
[...args],
|
|
132
|
-
{
|
|
133
|
-
cwd: runOptions.cwd,
|
|
134
|
-
env,
|
|
135
|
-
shell: false,
|
|
136
|
-
encoding: 'utf8',
|
|
137
|
-
timeout: timeoutMs,
|
|
138
|
-
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
139
|
-
windowsHide: true,
|
|
140
|
-
...(runOptions.signal ? { signal: runOptions.signal } : {}),
|
|
141
|
-
},
|
|
142
|
-
(error, stdout, stderr) =>
|
|
143
|
-
resolve(
|
|
144
|
-
toOutcome(error, stdout, stderr, {
|
|
145
|
-
executable,
|
|
146
|
-
timeoutMs,
|
|
147
|
-
signal: runOptions.signal,
|
|
148
|
-
}),
|
|
149
|
-
),
|
|
150
|
-
);
|
|
151
|
-
// stdin is a pipe by default; closing it at once turns a hook's read into EOF.
|
|
152
|
-
child.stdin?.end();
|
|
153
|
-
});
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* The reason a run did not produce a usable answer, for the verbs where a non-zero exit IS a failure.
|
|
160
|
-
* `undefined` when the child exited 0.
|
|
161
|
-
*/
|
|
162
|
-
export function gitFailureMessage(verb: string, outcome: TGitProcessOutcome): string | undefined {
|
|
163
|
-
if (outcome.kind === 'failed') return `git ${verb} failed: ${outcome.detail}`;
|
|
164
|
-
if (outcome.exitCode === 0) return undefined;
|
|
165
|
-
const stderr = outcome.stderr.trim();
|
|
166
|
-
return stderr.length > 0
|
|
167
|
-
? `git ${verb} failed (exit ${outcome.exitCode}): ${stderr}`
|
|
168
|
-
: `git ${verb} failed (exit ${outcome.exitCode})`;
|
|
169
|
-
}
|
package/src/git/git-status.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BEHAVIOR-2437: `/git status` — `git status --porcelain=v2 -z --branch`, parsed into the branch and
|
|
3
|
-
* the staged, unstaged, untracked (and, when present, conflicted) path sets.
|
|
4
|
-
*
|
|
5
|
-
* `-z` is what makes a path with a space, a quote, a newline or a non-ASCII character ONE element,
|
|
6
|
-
* and it is why a `2` (rename/copy) record is read as TWO NUL-terminated fields — `path`, then
|
|
7
|
-
* `origPath` — so the pair stays a pair.
|
|
8
|
-
*/
|
|
9
|
-
import { gitFailureMessage } from './git-process.js';
|
|
10
|
-
|
|
11
|
-
import type { IGitProcessPort } from './git-process.js';
|
|
12
|
-
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
13
|
-
|
|
14
|
-
export const GIT_STATUS_ARGS: readonly string[] = ['status', '--porcelain=v2', '-z', '--branch'];
|
|
15
|
-
|
|
16
|
-
export interface IGitStatusSummary {
|
|
17
|
-
/** `branch.head`; `(detached)` when detached; `undefined` when git printed no branch header. */
|
|
18
|
-
branch: string | undefined;
|
|
19
|
-
/** `branch.oid` is `(initial)` on an unborn branch. */
|
|
20
|
-
unborn: boolean;
|
|
21
|
-
staged: readonly string[];
|
|
22
|
-
unstaged: readonly string[];
|
|
23
|
-
untracked: readonly string[];
|
|
24
|
-
conflicted: readonly string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const ORDINARY = /^1 (\S\S) \S+ \S+ \S+ \S+ \S+ \S+ ([^]*)$/;
|
|
28
|
-
const RENAMED = /^2 (\S\S) \S+ \S+ \S+ \S+ \S+ \S+ \S+ ([^]*)$/;
|
|
29
|
-
const UNMERGED = /^u \S\S \S+ \S+ \S+ \S+ \S+ \S+ \S+ \S+ ([^]*)$/;
|
|
30
|
-
|
|
31
|
-
interface IStatusAccumulator {
|
|
32
|
-
branch: string | undefined;
|
|
33
|
-
unborn: boolean;
|
|
34
|
-
staged: string[];
|
|
35
|
-
unstaged: string[];
|
|
36
|
-
untracked: string[];
|
|
37
|
-
conflicted: string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function classify(acc: IStatusAccumulator, xy: string, display: string): void {
|
|
41
|
-
if (xy[0] !== '.') acc.staged.push(display);
|
|
42
|
-
if (xy[1] !== '.') acc.unstaged.push(display);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function readHeader(acc: IStatusAccumulator, record: string): void {
|
|
46
|
-
if (record.startsWith('# branch.head ')) acc.branch = record.slice('# branch.head '.length);
|
|
47
|
-
else if (record.startsWith('# branch.oid ')) {
|
|
48
|
-
acc.unborn = record.slice('# branch.oid '.length) === '(initial)';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Reads one record; returns how many EXTRA NUL-terminated fields it consumed (1 for a rename). */
|
|
53
|
-
function readRecord(acc: IStatusAccumulator, record: string, next: string | undefined): number {
|
|
54
|
-
if (record.startsWith('# ')) {
|
|
55
|
-
readHeader(acc, record);
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
if (record.startsWith('? ')) {
|
|
59
|
-
acc.untracked.push(record.slice(2));
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
if (record.startsWith('1 ')) {
|
|
63
|
-
const match = ORDINARY.exec(record);
|
|
64
|
-
if (match) classify(acc, match[1] ?? '..', match[2] ?? '');
|
|
65
|
-
return 0;
|
|
66
|
-
}
|
|
67
|
-
if (record.startsWith('2 ')) {
|
|
68
|
-
// The original path is the NEXT NUL-terminated field.
|
|
69
|
-
const match = RENAMED.exec(record);
|
|
70
|
-
if (match) classify(acc, match[1] ?? '..', `${match[2] ?? ''} (from ${next ?? ''})`);
|
|
71
|
-
return 1;
|
|
72
|
-
}
|
|
73
|
-
if (record.startsWith('u ')) {
|
|
74
|
-
const match = UNMERGED.exec(record);
|
|
75
|
-
if (match) acc.conflicted.push(match[1] ?? '');
|
|
76
|
-
}
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function parseStatusPorcelainV2(stdout: string): IGitStatusSummary {
|
|
81
|
-
const fields = stdout.split('\0');
|
|
82
|
-
const acc: IStatusAccumulator = {
|
|
83
|
-
branch: undefined,
|
|
84
|
-
unborn: false,
|
|
85
|
-
staged: [],
|
|
86
|
-
unstaged: [],
|
|
87
|
-
untracked: [],
|
|
88
|
-
conflicted: [],
|
|
89
|
-
};
|
|
90
|
-
for (let index = 0; index < fields.length; index += 1) {
|
|
91
|
-
const record = fields[index] ?? '';
|
|
92
|
-
if (record.length === 0) continue;
|
|
93
|
-
index += readRecord(acc, record, fields[index + 1]);
|
|
94
|
-
}
|
|
95
|
-
return acc;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function group(label: string, paths: readonly string[]): string {
|
|
99
|
-
return `${label} (${paths.length}):${paths.length > 0 ? ` ${paths.join(', ')}` : ''}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function formatGitStatus(summary: IGitStatusSummary): string {
|
|
103
|
-
const head =
|
|
104
|
-
summary.branch === undefined
|
|
105
|
-
? 'No branch information'
|
|
106
|
-
: summary.branch === '(detached)'
|
|
107
|
-
? 'HEAD detached'
|
|
108
|
-
: `On branch ${summary.branch}${summary.unborn ? ' (no commits yet)' : ''}`;
|
|
109
|
-
const lines = [
|
|
110
|
-
head,
|
|
111
|
-
group('staged', summary.staged),
|
|
112
|
-
group('unstaged', summary.unstaged),
|
|
113
|
-
group('untracked', summary.untracked),
|
|
114
|
-
];
|
|
115
|
-
if (summary.conflicted.length > 0) lines.push(group('conflicted', summary.conflicted));
|
|
116
|
-
return lines.join('\n');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function executeGitStatus(
|
|
120
|
-
port: IGitProcessPort,
|
|
121
|
-
cwd: string,
|
|
122
|
-
): Promise<ICommandResult> {
|
|
123
|
-
const outcome = await port.run(GIT_STATUS_ARGS, { cwd });
|
|
124
|
-
const failure = gitFailureMessage('status', outcome);
|
|
125
|
-
if (failure !== undefined || outcome.kind !== 'exited') {
|
|
126
|
-
return { success: false, message: failure ?? 'git status failed' };
|
|
127
|
-
}
|
|
128
|
-
const summary = parseStatusPorcelainV2(outcome.stdout);
|
|
129
|
-
return {
|
|
130
|
-
success: true,
|
|
131
|
-
message: formatGitStatus(summary),
|
|
132
|
-
data: { git: 'status', ...summary },
|
|
133
|
-
};
|
|
134
|
-
}
|