@robota-sdk/agent-command 3.0.0-beta.78 → 3.0.0-beta.81
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/README.md +11 -2
- package/dist/node/index.cjs +76 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +76 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +34 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +48 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +241 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +47 -0
- package/src/peers/peers-command.ts +142 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- package/src/plugins/default-plugin-command-source-loader.ts +0 -31
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor's evidence model (OBSERVABILITY-1991).
|
|
3
|
+
*
|
|
4
|
+
* `fail` is the only status that raises the exit code — the CLI-067 contract `robota diagnose`
|
|
5
|
+
* shipped with. `not-configured` names an absent optional capability out loud instead of treating
|
|
6
|
+
* it as healthy. `not-probed` is a CLOSED list of probes the read-only doctor deliberately does not
|
|
7
|
+
* run (see {@link TDoctorNotProbed}); an UNEXPECTED inability to probe is `warn` with its reason,
|
|
8
|
+
* never `not-probed` — the third state `enforcement-architecture.md` says must never read as a pass.
|
|
9
|
+
*/
|
|
10
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
11
|
+
import type {
|
|
12
|
+
IContributionSource,
|
|
13
|
+
ISkillRootDescriptor,
|
|
14
|
+
TSettingsSource,
|
|
15
|
+
TWorkspaceProjectAccess,
|
|
16
|
+
} from '@robota-sdk/agent-framework';
|
|
17
|
+
import type { ICommandMCPActivationAdapter } from '@robota-sdk/agent-framework';
|
|
18
|
+
|
|
19
|
+
export type TDoctorCheckStatus = 'ok' | 'warn' | 'fail' | 'not-configured' | 'not-probed';
|
|
20
|
+
|
|
21
|
+
/** The enumerated probes the read-only doctor does not run. Adding one is a spec change. */
|
|
22
|
+
export type TDoctorNotProbed = 'mcp-connection' | 'hook-execution' | 'owner-only-mode';
|
|
23
|
+
|
|
24
|
+
export interface IDoctorCheck {
|
|
25
|
+
/** Stable, user-typeable id (`settings.<scope>.<family>`, `storage.user`, `mcp.plugin.<id>.<server>`). */
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly status: TDoctorCheckStatus;
|
|
29
|
+
/** The exact file or directory the finding is about, when there is one. */
|
|
30
|
+
readonly path?: string;
|
|
31
|
+
/** The specific cause, as the owner reported it — a state word, an issue path, an errno; never a secret. */
|
|
32
|
+
readonly cause?: string;
|
|
33
|
+
/** Free-text detail lines rendered under the check. */
|
|
34
|
+
readonly detail?: readonly string[];
|
|
35
|
+
/** Present when this run can repair the finding; names the allowlisted repair id (equal to `id`). */
|
|
36
|
+
readonly repair?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IDoctorReport {
|
|
40
|
+
readonly checks: readonly IDoctorCheck[];
|
|
41
|
+
readonly failCount: number;
|
|
42
|
+
readonly warnCount: number;
|
|
43
|
+
/** Ids of checks whose `repair` is set in THIS run. */
|
|
44
|
+
readonly repairable: readonly string[];
|
|
45
|
+
/** Exit-code contract: `0` when `failCount === 0`, else `1`. */
|
|
46
|
+
readonly exitCode: 0 | 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One TCP reachability result; the probe is injected so tests never touch the network. */
|
|
50
|
+
export interface IDoctorEndpointProbeResult {
|
|
51
|
+
readonly reachable: boolean;
|
|
52
|
+
readonly elapsedMs?: number;
|
|
53
|
+
readonly error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What the host knows and the runner does not: the composition the shell built, and its facts. */
|
|
57
|
+
export interface IDoctorInputs {
|
|
58
|
+
readonly cwd: string;
|
|
59
|
+
readonly userHome: string;
|
|
60
|
+
/**
|
|
61
|
+
* The product's own user settings file — the only settings layer the repair allowlist may rewrite.
|
|
62
|
+
* The host names it; this library knows no product layout.
|
|
63
|
+
*/
|
|
64
|
+
readonly userSettingsPath: string;
|
|
65
|
+
/** The product's user store root and its sessions directory, as the host lays them out. */
|
|
66
|
+
readonly userStorage: { readonly root: string; readonly sessions: string };
|
|
67
|
+
/** The project state root under a trusted workspace, when the host has one. */
|
|
68
|
+
readonly projectStorageRoot?: string;
|
|
69
|
+
/** Every settings source the runtime merge chain reads, in precedence order. */
|
|
70
|
+
readonly settingsSources: readonly TSettingsSource[];
|
|
71
|
+
readonly projectAccess: TWorkspaceProjectAccess;
|
|
72
|
+
readonly providerDefinitions: readonly IProviderDefinition[];
|
|
73
|
+
/** Optional host wording for remediation; omitted values use product-neutral guidance. */
|
|
74
|
+
readonly diagnosticGuidance?: {
|
|
75
|
+
readonly providerResolution?: string;
|
|
76
|
+
readonly projectTrust?: string;
|
|
77
|
+
};
|
|
78
|
+
/** Environment map (test seam; default `process.env`). */
|
|
79
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
80
|
+
readonly contributionSources: readonly IContributionSource[];
|
|
81
|
+
readonly skillRoots: readonly ISkillRootDescriptor[];
|
|
82
|
+
/** Plugin scope directories, most specific first — the same list the session loader reads. */
|
|
83
|
+
readonly pluginsDirs: readonly string[];
|
|
84
|
+
/** Host-composed MCP activation adapter; absent means the CLI has no MCP host. */
|
|
85
|
+
readonly mcpActivation?: ICommandMCPActivationAdapter;
|
|
86
|
+
/** Checks only the host can make (Node version, terminal, CLI version); rendered first. */
|
|
87
|
+
readonly hostChecks: readonly IDoctorCheck[];
|
|
88
|
+
/** A workspace-composition failure the host converted into a check rather than a crash. */
|
|
89
|
+
readonly compositionFailure?: IDoctorCheck;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Storage facts about one path, probed without writing. */
|
|
93
|
+
export interface IDoctorPathFacts {
|
|
94
|
+
readonly exists: boolean;
|
|
95
|
+
readonly isDirectory: boolean;
|
|
96
|
+
readonly writable: boolean;
|
|
97
|
+
/** POSIX mode bits, or `undefined` when the platform does not assert them. */
|
|
98
|
+
readonly mode?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every side effect the runner needs, with Node defaults in `doctor-node-deps.ts`. */
|
|
102
|
+
export interface IDoctorDeps {
|
|
103
|
+
readonly probeEndpoint: (host: string, port: number) => Promise<IDoctorEndpointProbeResult>;
|
|
104
|
+
readonly inspectPath: (path: string) => IDoctorPathFacts;
|
|
105
|
+
/** `true` when a bare command name resolves on `PATH`, or a path-form command exists. */
|
|
106
|
+
readonly resolveCommand: (command: string) => boolean;
|
|
107
|
+
/** The platform's owner-only guarantee, from `@robota-sdk/agent-core/node`. */
|
|
108
|
+
readonly ownerOnlyGuarantee: () => 'posix-mode' | 'windows-acl';
|
|
109
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DoctorCommandSource,
|
|
3
|
+
createDoctorCommandEntry,
|
|
4
|
+
createDoctorCommandModule,
|
|
5
|
+
} from './doctor-command-module.js';
|
|
6
|
+
export { createNodeDoctorDeps, resolveCommandOnPath } from './doctor-node-deps.js';
|
|
7
|
+
export { renderDoctorReport } from './doctor-render.js';
|
|
8
|
+
export type { IDoctorDisplayVocabulary } from './doctor-render.js';
|
|
9
|
+
export { collectSettingsSecrets, redactDiagnosticText } from './doctor-redaction.js';
|
|
10
|
+
export {
|
|
11
|
+
STORAGE_REPAIR_ID,
|
|
12
|
+
doctorRepairAllowlist,
|
|
13
|
+
applyDoctorRepair,
|
|
14
|
+
isDoctorRepairId,
|
|
15
|
+
planDoctorRepair,
|
|
16
|
+
} from './doctor-repair.js';
|
|
17
|
+
export type {
|
|
18
|
+
IDoctorRepairPlan,
|
|
19
|
+
TDoctorRepairOutcome,
|
|
20
|
+
TDoctorRepairPlanResult,
|
|
21
|
+
} from './doctor-repair.js';
|
|
22
|
+
export { buildDoctorReport, runDoctor } from './doctor-runner.js';
|
|
23
|
+
export type {
|
|
24
|
+
IDoctorCheck,
|
|
25
|
+
IDoctorDeps,
|
|
26
|
+
IDoctorEndpointProbeResult,
|
|
27
|
+
IDoctorInputs,
|
|
28
|
+
IDoctorPathFacts,
|
|
29
|
+
IDoctorReport,
|
|
30
|
+
TDoctorCheckStatus,
|
|
31
|
+
TDoctorNotProbed,
|
|
32
|
+
} from './doctor-types.js';
|
|
@@ -6,17 +6,25 @@
|
|
|
6
6
|
* command opens the editor via `runWithTerminal`, captures the saved text, and cleans up — without a
|
|
7
7
|
* real interactive editor (which is a manual gate, TERM-002).
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
chmodSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
realpathSync,
|
|
17
|
+
} from 'node:fs';
|
|
10
18
|
import { tmpdir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
19
|
+
import { basename, dirname, join } from 'node:path';
|
|
12
20
|
|
|
13
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
21
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
22
|
|
|
15
23
|
import { scriptedSession, type ScriptedSessionHarness } from '@robota-sdk/agent-framework/testing';
|
|
16
24
|
|
|
17
25
|
import { createEditorCommandModule } from '../editor-command-module.js';
|
|
18
26
|
|
|
19
|
-
import type { ITerminalHandoff } from '@robota-sdk/agent-interface-
|
|
27
|
+
import type { ITerminalHandoff } from '@robota-sdk/agent-interface-session';
|
|
20
28
|
|
|
21
29
|
const TEST_TIMEOUT = 20_000;
|
|
22
30
|
|
|
@@ -31,32 +39,30 @@ function fakeHandoff(canHandoff: boolean): ITerminalHandoff {
|
|
|
31
39
|
|
|
32
40
|
/** Write a fake `$EDITOR`: a script that writes `content` into the file it is given as $1. */
|
|
33
41
|
function installFakeEditor(content: string): string {
|
|
34
|
-
const dir = mkdtempSync(join(tmpdir(), 'robota-fake-editor-'));
|
|
42
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-fake-editor-')));
|
|
35
43
|
const script = join(dir, 'fake-editor.sh');
|
|
36
|
-
writeFileSync(
|
|
44
|
+
writeFileSync(
|
|
45
|
+
script,
|
|
46
|
+
`#!/bin/sh\nprintf '%s' '${content}' > "$1"\nif [ -n "$EDITOR_CAPTURE_PATH" ]; then printf '%s' "$1" > "$EDITOR_CAPTURE_PATH"; fi\n`,
|
|
47
|
+
'utf8',
|
|
48
|
+
);
|
|
37
49
|
chmodSync(script, 0o755);
|
|
38
50
|
return script;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
const origEditor = process.env.EDITOR;
|
|
42
|
-
const origVisual = process.env.VISUAL;
|
|
43
|
-
|
|
44
53
|
let h: ScriptedSessionHarness | undefined;
|
|
45
54
|
afterEach(async () => {
|
|
46
55
|
await h?.dispose();
|
|
47
56
|
h = undefined;
|
|
48
|
-
|
|
49
|
-
else process.env.EDITOR = origEditor;
|
|
50
|
-
if (origVisual === undefined) delete process.env.VISUAL;
|
|
51
|
-
else process.env.VISUAL = origVisual;
|
|
57
|
+
vi.unstubAllEnvs();
|
|
52
58
|
});
|
|
53
59
|
|
|
54
60
|
describe('/editor command (framework functional)', () => {
|
|
55
61
|
it(
|
|
56
62
|
'opens the editor via the handoff and returns the saved text',
|
|
57
63
|
async () => {
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
vi.stubEnv('VISUAL', undefined);
|
|
65
|
+
vi.stubEnv('EDITOR', installFakeEditor('composed in editor'));
|
|
60
66
|
h = scriptedSession({
|
|
61
67
|
turns: [{ text: 'unused' }],
|
|
62
68
|
terminalHandoff: fakeHandoff(true),
|
|
@@ -72,10 +78,37 @@ describe('/editor command (framework functional)', () => {
|
|
|
72
78
|
TEST_TIMEOUT,
|
|
73
79
|
);
|
|
74
80
|
|
|
81
|
+
it(
|
|
82
|
+
'uses a neutral temporary directory and removes it after editing',
|
|
83
|
+
async () => {
|
|
84
|
+
const captureDir = mkdtempSync(join(tmpdir(), 'editor-capture-'));
|
|
85
|
+
const capturePath = join(captureDir, 'opened-path');
|
|
86
|
+
try {
|
|
87
|
+
vi.stubEnv('VISUAL', undefined);
|
|
88
|
+
vi.stubEnv('EDITOR', installFakeEditor('neutral draft'));
|
|
89
|
+
vi.stubEnv('EDITOR_CAPTURE_PATH', capturePath);
|
|
90
|
+
h = scriptedSession({
|
|
91
|
+
turns: [{ text: 'unused' }],
|
|
92
|
+
terminalHandoff: fakeHandoff(true),
|
|
93
|
+
commandModules: [createEditorCommandModule()],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = await h.command('editor', '');
|
|
97
|
+
const openedPath = readFileSync(capturePath, 'utf8');
|
|
98
|
+
expect(result?.message).toBe('neutral draft');
|
|
99
|
+
expect(basename(dirname(openedPath))).toMatch(/^agent-editor-/);
|
|
100
|
+
expect(existsSync(dirname(openedPath))).toBe(false);
|
|
101
|
+
} finally {
|
|
102
|
+
rmSync(captureDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
TEST_TIMEOUT,
|
|
106
|
+
);
|
|
107
|
+
|
|
75
108
|
it(
|
|
76
109
|
'is unavailable when there is no interactive terminal',
|
|
77
110
|
async () => {
|
|
78
|
-
|
|
111
|
+
vi.stubEnv('EDITOR', installFakeEditor('unused'));
|
|
79
112
|
h = scriptedSession({
|
|
80
113
|
turns: [{ text: 'unused' }],
|
|
81
114
|
commandModules: [createEditorCommandModule()],
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { EDITOR_COMMAND_DESCRIPTION, executeEditorCommand } from './editor-command.js';
|
|
5
5
|
|
|
6
6
|
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
7
|
-
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-
|
|
7
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
8
8
|
|
|
9
9
|
export function createEditorCommandEntry(): ICommand {
|
|
10
10
|
return {
|
|
@@ -12,11 +12,12 @@ export function createEditorCommandEntry(): ICommand {
|
|
|
12
12
|
displayName: 'Editor',
|
|
13
13
|
description: EDITOR_COMMAND_DESCRIPTION,
|
|
14
14
|
source: 'editor',
|
|
15
|
+
// User-only: opens the user's terminal editor; UI-only.
|
|
15
16
|
modelInvocable: false,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function createEditorSystemCommand(): ISystemCommand {
|
|
20
|
+
function createEditorSystemCommand(temporaryDirectoryPrefix?: string): ISystemCommand {
|
|
20
21
|
const entry = createEditorCommandEntry();
|
|
21
22
|
return {
|
|
22
23
|
name: entry.name,
|
|
@@ -26,7 +27,7 @@ function createEditorSystemCommand(): ISystemCommand {
|
|
|
26
27
|
userInvocable: true,
|
|
27
28
|
modelInvocable: false,
|
|
28
29
|
lifecycle: 'inline',
|
|
29
|
-
execute: executeEditorCommand,
|
|
30
|
+
execute: (context, args) => executeEditorCommand(context, args, temporaryDirectoryPrefix),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -38,10 +39,10 @@ export class EditorCommandSource implements ICommandSource {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export function createEditorCommandModule(): ICommandModule {
|
|
42
|
+
export function createEditorCommandModule(temporaryDirectoryPrefix?: string): ICommandModule {
|
|
42
43
|
return {
|
|
43
44
|
name: 'agent-command-editor',
|
|
44
45
|
commandSources: [new EditorCommandSource()],
|
|
45
|
-
systemCommands: [createEditorSystemCommand()],
|
|
46
|
+
systemCommands: [createEditorSystemCommand(temporaryDirectoryPrefix)],
|
|
46
47
|
};
|
|
47
48
|
}
|
|
@@ -9,17 +9,22 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { resolveEditor } from './resolve-editor.js';
|
|
10
10
|
import { spawnInherited } from '../shell/spawn-inherited.js';
|
|
11
11
|
|
|
12
|
-
import type {
|
|
13
|
-
|
|
12
|
+
import type {
|
|
13
|
+
ICommandHostTerminalHandoff,
|
|
14
|
+
ICommandHostWorkspace,
|
|
15
|
+
} from '@robota-sdk/agent-framework';
|
|
16
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
14
17
|
|
|
15
18
|
export const EDITOR_COMMAND_DESCRIPTION =
|
|
16
19
|
'Compose a message in $EDITOR (optionally pre-filled with `/editor <text>`), then return it.';
|
|
20
|
+
export const DEFAULT_EDITOR_TEMPORARY_DIRECTORY_PREFIX = 'agent-editor-';
|
|
17
21
|
|
|
18
22
|
export async function executeEditorCommand(
|
|
19
|
-
context:
|
|
23
|
+
context: ICommandHostTerminalHandoff & ICommandHostWorkspace,
|
|
20
24
|
args: string,
|
|
25
|
+
temporaryDirectoryPrefix: string = DEFAULT_EDITOR_TEMPORARY_DIRECTORY_PREFIX,
|
|
21
26
|
): Promise<ICommandResult> {
|
|
22
|
-
if (context.canHandoffTerminal
|
|
27
|
+
if (!context.canHandoffTerminal()) {
|
|
23
28
|
return {
|
|
24
29
|
message: 'An editor is unavailable here (no interactive terminal).',
|
|
25
30
|
success: false,
|
|
@@ -27,7 +32,7 @@ export async function executeEditorCommand(
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const editor = resolveEditor();
|
|
30
|
-
const dir = mkdtempSync(join(tmpdir(),
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), temporaryDirectoryPrefix));
|
|
31
36
|
const file = join(dir, 'message.md');
|
|
32
37
|
writeFileSync(file, args ?? '', 'utf8');
|
|
33
38
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MODEL_EFFORT_VALUES } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
import { executeEffortCommand } from './effort-command.js';
|
|
4
|
+
|
|
5
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
6
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
7
|
+
|
|
8
|
+
const EFFORT_ARGUMENT_HINT = `auto | ${MODEL_EFFORT_VALUES.join(' | ')}`;
|
|
9
|
+
|
|
10
|
+
export function createEffortCommandEntry(): ICommand {
|
|
11
|
+
return {
|
|
12
|
+
name: 'effort',
|
|
13
|
+
displayName: 'Model Effort',
|
|
14
|
+
description: 'Show or change the model effort level',
|
|
15
|
+
source: 'effort',
|
|
16
|
+
argumentHint: EFFORT_ARGUMENT_HINT,
|
|
17
|
+
// User-only: the effort tier is the user's cost/latency preference.
|
|
18
|
+
modelInvocable: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createEffortSystemCommand(): ISystemCommand {
|
|
23
|
+
const entry = createEffortCommandEntry();
|
|
24
|
+
return {
|
|
25
|
+
name: entry.name,
|
|
26
|
+
displayName: entry.displayName,
|
|
27
|
+
description: entry.description,
|
|
28
|
+
requiresPermission: false,
|
|
29
|
+
userInvocable: true,
|
|
30
|
+
modelInvocable: false,
|
|
31
|
+
argumentHint: entry.argumentHint,
|
|
32
|
+
lifecycle: 'inline',
|
|
33
|
+
execute: executeEffortCommand,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class EffortCommandSource implements ICommandSource {
|
|
38
|
+
readonly name = 'effort';
|
|
39
|
+
|
|
40
|
+
getCommands(): ICommand[] {
|
|
41
|
+
return [createEffortCommandEntry()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createEffortCommandModule(): ICommandModule {
|
|
46
|
+
return {
|
|
47
|
+
name: 'agent-command-effort',
|
|
48
|
+
commandSources: [new EffortCommandSource()],
|
|
49
|
+
systemCommands: [createEffortSystemCommand()],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { executeEffortCommand } from './effort-command.js';
|
|
4
|
+
|
|
5
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
6
|
+
|
|
7
|
+
describe('/effort', () => {
|
|
8
|
+
it('applies a requested level and returns the complete resolution', async () => {
|
|
9
|
+
const applyModelOptions = vi.fn();
|
|
10
|
+
const writeSettings = vi.fn();
|
|
11
|
+
const host = createTestCommandHost({
|
|
12
|
+
session: { applyModelOptions },
|
|
13
|
+
overrides: {
|
|
14
|
+
getCommandHostAdapters: () => ({
|
|
15
|
+
settings: { read: () => ({ theme: 'brief' }), write: writeSettings },
|
|
16
|
+
effort: {
|
|
17
|
+
getResolution: () => ({
|
|
18
|
+
requested: 'high',
|
|
19
|
+
effective: 'high',
|
|
20
|
+
source: 'model-default',
|
|
21
|
+
disposition: 'model-default',
|
|
22
|
+
modelDefault: 'high',
|
|
23
|
+
}),
|
|
24
|
+
apply: async (selection) => ({
|
|
25
|
+
requested: selection,
|
|
26
|
+
effective: selection === 'auto' ? 'high' : selection,
|
|
27
|
+
source: 'command',
|
|
28
|
+
disposition: selection === 'auto' ? 'model-default' : 'applied',
|
|
29
|
+
modelDefault: 'high',
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await executeEffortCommand(host, 'low');
|
|
37
|
+
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
expect(result.data).toMatchObject({
|
|
40
|
+
effort: { requested: 'low', effective: 'low', source: 'command', disposition: 'applied' },
|
|
41
|
+
});
|
|
42
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
43
|
+
expect(writeSettings).toHaveBeenCalledWith({ theme: 'brief', effort: 'low' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reports the current value without changing it when headless selection has no argument', async () => {
|
|
47
|
+
const applyModelOptions = vi.fn();
|
|
48
|
+
const host = createTestCommandHost({ session: { applyModelOptions } });
|
|
49
|
+
|
|
50
|
+
const result = await executeEffortCommand(host, '');
|
|
51
|
+
|
|
52
|
+
expect(result.success).toBe(true);
|
|
53
|
+
expect(result.message).toContain('high');
|
|
54
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('reports an unadapted auto selection without treating auto as a concrete effective tier', async () => {
|
|
58
|
+
const host = createTestCommandHost({
|
|
59
|
+
session: { getModelEffort: () => 'auto' },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = await executeEffortCommand(host, '');
|
|
63
|
+
|
|
64
|
+
expect(result.data).toMatchObject({
|
|
65
|
+
effort: { requested: 'auto', effective: 'high', disposition: 'model-default' },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('removes a persisted explicit value when auto is selected', async () => {
|
|
70
|
+
const writeSettings = vi.fn();
|
|
71
|
+
const host = createTestCommandHost({
|
|
72
|
+
overrides: {
|
|
73
|
+
getCommandHostAdapters: () => ({
|
|
74
|
+
settings: { read: () => ({ effort: 'low', theme: 'brief' }), write: writeSettings },
|
|
75
|
+
effort: {
|
|
76
|
+
getResolution: () => ({
|
|
77
|
+
requested: 'low',
|
|
78
|
+
effective: 'low',
|
|
79
|
+
source: 'settings',
|
|
80
|
+
disposition: 'applied',
|
|
81
|
+
modelDefault: 'high',
|
|
82
|
+
}),
|
|
83
|
+
apply: async () => ({
|
|
84
|
+
requested: 'auto',
|
|
85
|
+
effective: 'high',
|
|
86
|
+
source: 'command',
|
|
87
|
+
disposition: 'model-default',
|
|
88
|
+
modelDefault: 'high',
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await executeEffortCommand(host, 'auto');
|
|
96
|
+
|
|
97
|
+
expect(writeSettings).toHaveBeenCalledWith({ theme: 'brief' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('preserves auto in the session when no effort adapter is installed', async () => {
|
|
101
|
+
const applyModelOptions = vi.fn();
|
|
102
|
+
const host = createTestCommandHost({ session: { applyModelOptions } });
|
|
103
|
+
|
|
104
|
+
const result = await executeEffortCommand(host, 'auto');
|
|
105
|
+
|
|
106
|
+
expect(result.success).toBe(true);
|
|
107
|
+
expect(applyModelOptions).toHaveBeenCalledWith({ effort: 'auto' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('leaves the live value and settings unchanged when the picker is cancelled', async () => {
|
|
111
|
+
const applyModelOptions = vi.fn();
|
|
112
|
+
const writeSettings = vi.fn();
|
|
113
|
+
const host = createTestCommandHost({
|
|
114
|
+
session: { applyModelOptions },
|
|
115
|
+
overrides: {
|
|
116
|
+
getUserInteraction: () => ({ ask: async () => ({ type: 'cancelled' }) }),
|
|
117
|
+
getCommandHostAdapters: () => ({
|
|
118
|
+
settings: { read: () => ({ effort: 'medium' }), write: writeSettings },
|
|
119
|
+
effort: {
|
|
120
|
+
getResolution: () => ({
|
|
121
|
+
requested: 'medium',
|
|
122
|
+
effective: 'medium',
|
|
123
|
+
source: 'settings',
|
|
124
|
+
disposition: 'applied',
|
|
125
|
+
modelDefault: 'high',
|
|
126
|
+
}),
|
|
127
|
+
apply: vi.fn(),
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await executeEffortCommand(host, '');
|
|
134
|
+
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
expect(result.message).toContain('effective=medium');
|
|
137
|
+
expect(applyModelOptions).not.toHaveBeenCalled();
|
|
138
|
+
expect(writeSettings).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { MODEL_EFFORT_VALUES, selectAction } from '@robota-sdk/agent-core';
|
|
2
|
+
import { parseModelEffort, resolveModelEffort } from '@robota-sdk/agent-framework';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ICommandHostAdapterAccess,
|
|
6
|
+
ICommandHostAdapters,
|
|
7
|
+
ICommandHostSessionAccess,
|
|
8
|
+
ICommandHostUserInteraction,
|
|
9
|
+
ICommandHostWorkspace,
|
|
10
|
+
IModelEffortResolution,
|
|
11
|
+
TEffortSelection,
|
|
12
|
+
} from '@robota-sdk/agent-framework';
|
|
13
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
14
|
+
|
|
15
|
+
const EFFORT_SELECTIONS: readonly TEffortSelection[] = ['auto', ...MODEL_EFFORT_VALUES];
|
|
16
|
+
|
|
17
|
+
function formatEffortMessage(resolution: {
|
|
18
|
+
requested: TEffortSelection;
|
|
19
|
+
effective: string;
|
|
20
|
+
source: string;
|
|
21
|
+
disposition: string;
|
|
22
|
+
}): string {
|
|
23
|
+
return `Model effort: requested=${resolution.requested}, effective=${resolution.effective}, source=${resolution.source}, disposition=${resolution.disposition}.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function askForEffort(
|
|
27
|
+
context: ICommandHostUserInteraction,
|
|
28
|
+
): Promise<TEffortSelection | undefined> {
|
|
29
|
+
const interaction = context.getUserInteraction();
|
|
30
|
+
if (interaction === undefined) return undefined;
|
|
31
|
+
const response = await interaction.ask(
|
|
32
|
+
selectAction(
|
|
33
|
+
'effort',
|
|
34
|
+
'Select model effort',
|
|
35
|
+
EFFORT_SELECTIONS.map((value) => ({ value, label: value })),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
const selected = response.type === 'answer' ? response.values[0] : undefined;
|
|
39
|
+
return parseModelEffort(selected, '/effort');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readCurrentResolution(
|
|
43
|
+
context: ICommandHostAdapterAccess & ICommandHostSessionAccess,
|
|
44
|
+
): IModelEffortResolution {
|
|
45
|
+
const adapter = context.getCommandHostAdapters?.().effort;
|
|
46
|
+
if (adapter !== undefined) return adapter.getResolution();
|
|
47
|
+
const selection = context.getSession().getModelEffort();
|
|
48
|
+
return resolveModelEffort({
|
|
49
|
+
command: selection,
|
|
50
|
+
modelDefault: selection === 'auto' ? 'high' : selection,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readAdapters(context: ICommandHostAdapterAccess): ICommandHostAdapters {
|
|
55
|
+
return context.getCommandHostAdapters?.() ?? {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function persistEffortSelection(
|
|
59
|
+
context: ICommandHostAdapterAccess,
|
|
60
|
+
selection: TEffortSelection,
|
|
61
|
+
): void {
|
|
62
|
+
const settings = readAdapters(context).settings;
|
|
63
|
+
if (settings === undefined || selection === 'max') return;
|
|
64
|
+
const current = settings.read();
|
|
65
|
+
if (selection === 'auto') {
|
|
66
|
+
const { effort: _effort, ...withoutEffort } = current;
|
|
67
|
+
settings.write(withoutEffort);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settings.write({ ...current, effort: selection });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function executeEffortCommand(
|
|
74
|
+
context: ICommandHostAdapterAccess &
|
|
75
|
+
ICommandHostSessionAccess &
|
|
76
|
+
ICommandHostUserInteraction &
|
|
77
|
+
ICommandHostWorkspace,
|
|
78
|
+
args: string,
|
|
79
|
+
): Promise<ICommandResult> {
|
|
80
|
+
let selection: TEffortSelection | undefined;
|
|
81
|
+
try {
|
|
82
|
+
selection = parseModelEffort(args.trim() || undefined, '/effort');
|
|
83
|
+
if (selection === undefined) selection = await askForEffort(context);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return { message: error instanceof Error ? error.message : String(error), success: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const current = readCurrentResolution(context);
|
|
89
|
+
if (selection === undefined) {
|
|
90
|
+
return {
|
|
91
|
+
message: formatEffortMessage(current),
|
|
92
|
+
success: true,
|
|
93
|
+
data: { effort: current },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const adapter = readAdapters(context).effort;
|
|
98
|
+
let resolution;
|
|
99
|
+
try {
|
|
100
|
+
resolution =
|
|
101
|
+
adapter === undefined
|
|
102
|
+
? resolveModelEffort({ command: selection, modelDefault: current.effective })
|
|
103
|
+
: await adapter.apply(selection, context.getSession());
|
|
104
|
+
if (adapter === undefined) {
|
|
105
|
+
await context.getSession().applyModelOptions({ effort: selection });
|
|
106
|
+
}
|
|
107
|
+
if (context.getCommandInvocationSource() === 'user') {
|
|
108
|
+
persistEffortSelection(context, selection);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
message: `Model effort was not applied: ${error instanceof Error ? error.message : String(error)}`,
|
|
113
|
+
success: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
message: formatEffortMessage(resolution),
|
|
119
|
+
success: true,
|
|
120
|
+
data: { effort: resolution },
|
|
121
|
+
};
|
|
122
|
+
}
|