@link-assistant/agent 0.22.10 → 0.24.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.
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/cli/continuous-mode.js +142 -27
- package/src/cli/event-handler.js +21 -0
- package/src/cli/input-queue.js +232 -12
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +43 -0
- package/src/config/config.ts +37 -0
- package/src/index.js +83 -23
- package/src/permission/index.ts +564 -0
- package/src/session/prompt.ts +6 -0
- package/src/session/system.ts +14 -0
- package/src/tool/bash.ts +41 -1
- package/src/tool/batch.ts +8 -0
- package/src/tool/edit.ts +12 -1
- package/src/tool/patch.ts +19 -1
- package/src/tool/registry.ts +36 -0
- package/src/tool/webfetch.ts +12 -1
- package/src/tool/write.ts +13 -1
package/src/config/config.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface AgentConfig {
|
|
|
52
52
|
mcpDefaultToolCallTimeout: number;
|
|
53
53
|
mcpMaxToolCallTimeout: number;
|
|
54
54
|
verifyImagesAtReadTool: boolean;
|
|
55
|
+
readOnly: boolean;
|
|
56
|
+
disableTools: string;
|
|
57
|
+
permissionMode: string;
|
|
58
|
+
permission: string;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// Fallback helpers for when config is not yet initialized (early imports/tests)
|
|
@@ -132,6 +136,10 @@ function defaultConfig(): AgentConfig {
|
|
|
132
136
|
})(),
|
|
133
137
|
verifyImagesAtReadTool:
|
|
134
138
|
getEnvStr('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL') !== 'false',
|
|
139
|
+
readOnly: truthyEnv('LINK_ASSISTANT_AGENT_READ_ONLY'),
|
|
140
|
+
disableTools: getEnvStr('LINK_ASSISTANT_AGENT_DISABLE_TOOLS') ?? '',
|
|
141
|
+
permissionMode: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION_MODE') ?? 'auto',
|
|
142
|
+
permission: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION') ?? '',
|
|
135
143
|
};
|
|
136
144
|
}
|
|
137
145
|
|
|
@@ -240,6 +248,31 @@ function buildAgentConfigOptions({
|
|
|
240
248
|
type: 'boolean',
|
|
241
249
|
description: 'Verify images when using the read tool',
|
|
242
250
|
default: getenv('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL', true),
|
|
251
|
+
})
|
|
252
|
+
.option('read-only', {
|
|
253
|
+
type: 'boolean',
|
|
254
|
+
description:
|
|
255
|
+
'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch).',
|
|
256
|
+
default: getenv('LINK_ASSISTANT_AGENT_READ_ONLY', false),
|
|
257
|
+
})
|
|
258
|
+
.option('disable-tools', {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description:
|
|
261
|
+
'Comma-separated list of tool ids to disable (e.g. "bash,write,edit"). Disabled tools are never exposed to the model and are rejected if invoked via batch.',
|
|
262
|
+
default: getenv('LINK_ASSISTANT_AGENT_DISABLE_TOOLS', ''),
|
|
263
|
+
})
|
|
264
|
+
.option('permission-mode', {
|
|
265
|
+
type: 'string',
|
|
266
|
+
choices: ['auto', 'plan', 'readonly', 'ask'],
|
|
267
|
+
description:
|
|
268
|
+
'Permission policy for mutating tools. "auto" (default) allows everything; "plan" allows read-only shell and asks before mutations; "readonly" denies all mutations; "ask" asks before every mutating tool. Approvals are driven over JSON (permission_request / permission_response).',
|
|
269
|
+
default: getenv('LINK_ASSISTANT_AGENT_PERMISSION_MODE', 'auto'),
|
|
270
|
+
})
|
|
271
|
+
.option('permission', {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description:
|
|
274
|
+
'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
|
|
275
|
+
default: getenv('LINK_ASSISTANT_AGENT_PERMISSION', ''),
|
|
243
276
|
});
|
|
244
277
|
}
|
|
245
278
|
|
|
@@ -292,6 +325,10 @@ export function initConfig(argv?: string[]): AgentConfig {
|
|
|
292
325
|
mcpDefaultToolCallTimeout: parsed.mcpDefaultToolCallTimeout ?? 120000,
|
|
293
326
|
mcpMaxToolCallTimeout: parsed.mcpMaxToolCallTimeout ?? 600000,
|
|
294
327
|
verifyImagesAtReadTool: parsed.verifyImagesAtReadTool ?? true,
|
|
328
|
+
readOnly: parsed.readOnly ?? false,
|
|
329
|
+
disableTools: parsed.disableTools ?? '',
|
|
330
|
+
permissionMode: parsed.permissionMode ?? 'auto',
|
|
331
|
+
permission: parsed.permission ?? '',
|
|
295
332
|
});
|
|
296
333
|
|
|
297
334
|
// Propagate verbose to env var for subprocess resilience (issue #227).
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runContinuousDirectMode,
|
|
37
37
|
resolveResumeSession,
|
|
38
38
|
} from './cli/continuous-mode.js';
|
|
39
|
+
import { parseStreamJsonInput } from './cli/input-queue.js';
|
|
39
40
|
import { createBusEventSubscription } from './cli/event-handler.js';
|
|
40
41
|
import {
|
|
41
42
|
outputStatus,
|
|
@@ -259,6 +260,19 @@ async function readSystemMessages(argv) {
|
|
|
259
260
|
return { systemMessage, appendSystemMessage };
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
function getInputFormat(argv) {
|
|
264
|
+
return argv['input-format'] || argv.inputFormat || 'text';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getAcceptedInputFormats(inputFormat, isInteractive) {
|
|
268
|
+
if (inputFormat === 'stream-json') {
|
|
269
|
+
return ['Claude stream-json user frames'];
|
|
270
|
+
}
|
|
271
|
+
return isInteractive
|
|
272
|
+
? ['JSON object with "message" field', 'Plain text']
|
|
273
|
+
: ['JSON object with "message" field'];
|
|
274
|
+
}
|
|
275
|
+
|
|
262
276
|
async function runAgentMode(argv, request) {
|
|
263
277
|
// Log version and command info in verbose mode using lazy logging
|
|
264
278
|
Log.Default.info(() => ({
|
|
@@ -663,8 +677,15 @@ async function main() {
|
|
|
663
677
|
// Check if stdin is a TTY (interactive terminal)
|
|
664
678
|
if (process.stdin.isTTY) {
|
|
665
679
|
// Enter interactive terminal mode with continuous listening
|
|
666
|
-
const
|
|
667
|
-
const
|
|
680
|
+
const inputFormat = getInputFormat(argv);
|
|
681
|
+
const isInteractive =
|
|
682
|
+
inputFormat === 'stream-json'
|
|
683
|
+
? false
|
|
684
|
+
: argv.interactive !== false;
|
|
685
|
+
const autoMerge =
|
|
686
|
+
inputFormat === 'stream-json'
|
|
687
|
+
? false
|
|
688
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
668
689
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
669
690
|
|
|
670
691
|
// Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
|
|
@@ -688,10 +709,12 @@ async function main() {
|
|
|
688
709
|
message:
|
|
689
710
|
'Agent CLI in interactive terminal mode. Type your message and press Enter.',
|
|
690
711
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
691
|
-
acceptedFormats:
|
|
692
|
-
|
|
693
|
-
|
|
712
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
713
|
+
inputFormat,
|
|
714
|
+
isInteractive
|
|
715
|
+
),
|
|
694
716
|
options: {
|
|
717
|
+
inputFormat,
|
|
695
718
|
interactive: isInteractive,
|
|
696
719
|
autoMergeQueuedMessages: autoMerge,
|
|
697
720
|
alwaysAcceptStdin,
|
|
@@ -707,8 +730,13 @@ async function main() {
|
|
|
707
730
|
}
|
|
708
731
|
|
|
709
732
|
// stdin is piped - enter stdin listening mode
|
|
710
|
-
const
|
|
711
|
-
const
|
|
733
|
+
const inputFormat = getInputFormat(argv);
|
|
734
|
+
const isInteractive =
|
|
735
|
+
inputFormat === 'stream-json' ? false : argv.interactive !== false;
|
|
736
|
+
const autoMerge =
|
|
737
|
+
inputFormat === 'stream-json'
|
|
738
|
+
? false
|
|
739
|
+
: argv['auto-merge-queued-messages'] !== false;
|
|
712
740
|
const alwaysAcceptStdin = argv['always-accept-stdin'] !== false;
|
|
713
741
|
|
|
714
742
|
outputStatus(
|
|
@@ -719,10 +747,12 @@ async function main() {
|
|
|
719
747
|
? 'Agent CLI in continuous listening mode. Accepts JSON and plain text input.'
|
|
720
748
|
: 'Agent CLI in single-message mode. Accepts JSON and plain text input.',
|
|
721
749
|
hint: 'Press CTRL+C to exit. Use --help for options.',
|
|
722
|
-
acceptedFormats:
|
|
723
|
-
|
|
724
|
-
|
|
750
|
+
acceptedFormats: getAcceptedInputFormats(
|
|
751
|
+
inputFormat,
|
|
752
|
+
isInteractive
|
|
753
|
+
),
|
|
725
754
|
options: {
|
|
755
|
+
inputFormat,
|
|
726
756
|
interactive: isInteractive,
|
|
727
757
|
autoMergeQueuedMessages: autoMerge,
|
|
728
758
|
alwaysAcceptStdin,
|
|
@@ -757,27 +787,52 @@ async function main() {
|
|
|
757
787
|
|
|
758
788
|
// Try to parse as JSON, if it fails treat it as plain text message
|
|
759
789
|
let request;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
790
|
+
if (inputFormat === 'stream-json') {
|
|
791
|
+
try {
|
|
792
|
+
const firstFrame = trimmedInput
|
|
793
|
+
.split(/\r?\n/)
|
|
794
|
+
.find((line) => line.trim());
|
|
795
|
+
request = parseStreamJsonInput(firstFrame || '');
|
|
796
|
+
if (request.kind !== 'message') {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Expected stream-json user frame, received ${request.kind}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
766
802
|
outputError(
|
|
767
803
|
{
|
|
768
804
|
errorType: 'ValidationError',
|
|
769
805
|
message:
|
|
770
|
-
|
|
771
|
-
hint: '
|
|
806
|
+
error instanceof Error ? error.message : String(error),
|
|
807
|
+
hint: 'Provide one Claude stream-json user frame per line.',
|
|
772
808
|
},
|
|
773
809
|
compactJson
|
|
774
810
|
);
|
|
775
811
|
process.exit(1);
|
|
776
812
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
813
|
+
} else {
|
|
814
|
+
try {
|
|
815
|
+
request = JSON.parse(trimmedInput);
|
|
816
|
+
} catch (_e) {
|
|
817
|
+
// Not JSON
|
|
818
|
+
if (!isInteractive) {
|
|
819
|
+
// In non-interactive mode, only accept JSON
|
|
820
|
+
outputError(
|
|
821
|
+
{
|
|
822
|
+
errorType: 'ValidationError',
|
|
823
|
+
message:
|
|
824
|
+
'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
|
|
825
|
+
hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
|
|
826
|
+
},
|
|
827
|
+
compactJson
|
|
828
|
+
);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
// In interactive mode, treat as plain text message
|
|
832
|
+
request = {
|
|
833
|
+
message: trimmedInput,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
781
836
|
}
|
|
782
837
|
|
|
783
838
|
// Output input confirmation in JSON format
|
|
@@ -785,7 +840,12 @@ async function main() {
|
|
|
785
840
|
{
|
|
786
841
|
raw: trimmedInput,
|
|
787
842
|
parsed: request,
|
|
788
|
-
format:
|
|
843
|
+
format:
|
|
844
|
+
inputFormat === 'stream-json'
|
|
845
|
+
? 'stream-json'
|
|
846
|
+
: isInteractive
|
|
847
|
+
? 'text'
|
|
848
|
+
: 'json',
|
|
789
849
|
},
|
|
790
850
|
compactJson
|
|
791
851
|
);
|