@borgee/agents-host 0.2.33 → 0.2.35
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 +28 -7
- package/dist/agents-host.d.ts +17 -0
- package/dist/agents-host.js +52 -0
- package/dist/chat/chat-control-plane.d.ts +9 -0
- package/dist/chat/sdk-chat-control-plane.d.ts +10 -1
- package/dist/chat/sdk-chat-control-plane.js +6 -0
- package/dist/cli-args.d.ts +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/compatibility-gates.d.ts +1 -0
- package/dist/compatibility-gates.js +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +21 -3
- package/dist/context/main-session-delegation.d.ts +1 -0
- package/dist/context/main-session-delegation.js +6 -0
- package/dist/context/prompt.js +4 -2
- package/dist/gateway/localhost-gateway.js +66 -0
- package/dist/local-config.js +10 -1
- package/dist/managed-daemon.js +5 -0
- package/dist/plugin-sdk.js +58 -1
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/gateway-authorization.d.ts +18 -3
- package/dist/policy/gateway-authorization.js +33 -1
- package/dist/providers/claude/adapter.js +3 -1
- package/dist/providers/claude/cli-client.d.ts +25 -1
- package/dist/providers/claude/cli-client.js +127 -10
- package/dist/providers/codex/adapter.js +3 -1
- package/dist/providers/codex/cli-client.d.ts +25 -1
- package/dist/providers/codex/cli-client.js +127 -10
- package/dist/providers/codex/project-doc.js +3 -0
- package/dist/providers/copilot/adapter.js +3 -1
- package/dist/providers/copilot/cli-client.d.ts +25 -1
- package/dist/providers/copilot/cli-client.js +125 -10
- package/dist/providers/create-provider.js +14 -9
- package/dist/providers/idle-backend-shutdown.d.ts +16 -0
- package/dist/providers/idle-backend-shutdown.js +53 -0
- package/dist/types.d.ts +7 -0
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +19 -2
- package/skills/borgee-agent/borgee-agent.mjs +56 -1
- package/skills/borgee-agent/borgee-agent.py +33 -2
|
@@ -2,6 +2,7 @@ import { FileChannelContextStore } from '../context/injection.js';
|
|
|
2
2
|
import { createProviderConnectionsSessionStore } from '../connections-state-store.js';
|
|
3
3
|
import { ProviderTurnPreparer } from '../context/turn-preparation.js';
|
|
4
4
|
import { CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE, CONTEXT_INJECTION_COMPATIBILITY_GATE, CODEX_PROVIDER_COMPATIBILITY_GATE, COPILOT_PROVIDER_V2_COMPATIBILITY_GATE, INTERNAL_PROVIDER_IMPLEMENTATIONS_ENV, LOCALHOST_GATEWAY_COMPATIBILITY_GATE, parseInternalProviderImplementationOverrides, POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE, resolveInternalPolicyMode, resolveInternalCompatibilityGates, SKILL_RUNTIME_COMPATIBILITY_GATE, } from '../compatibility-gates.js';
|
|
5
|
+
import { resolveIdleBackendShutdownMs } from './idle-backend-shutdown.js';
|
|
5
6
|
import { ClaudeCliClient } from './claude/cli-client.js';
|
|
6
7
|
import { ClaudeProviderAdapter } from './claude/adapter.js';
|
|
7
8
|
import { FileClaudeChannelSessionStore } from './claude/session-store.js';
|
|
@@ -39,7 +40,9 @@ class CliBackedProviderV2 {
|
|
|
39
40
|
const text = await this.cli.generateReply(input.turn, {
|
|
40
41
|
onProgress: createAwaitingUserProgressHandler(options?.onProgress),
|
|
41
42
|
});
|
|
42
|
-
|
|
43
|
+
// Read the session AFTER the turn: a first turn on a channel opens the
|
|
44
|
+
// session as it runs, so before this point there is nothing to report.
|
|
45
|
+
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.turn.channelId) };
|
|
43
46
|
}
|
|
44
47
|
async shutdown() {
|
|
45
48
|
await this.cli.dispose();
|
|
@@ -57,8 +60,8 @@ function resolveCopilotImplementation(routingConfig) {
|
|
|
57
60
|
}
|
|
58
61
|
return routingConfig.implementationOverrides.copilot ?? 'v2';
|
|
59
62
|
}
|
|
60
|
-
function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer) {
|
|
61
|
-
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs, {}, createProviderConnectionsSessionStore({
|
|
63
|
+
function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer, idleBackendShutdownMs) {
|
|
64
|
+
const cli = new ClaudeCliClient(config.claudeCommand, config.claudeArgs, { idleBackendShutdownMs }, createProviderConnectionsSessionStore({
|
|
62
65
|
provider: 'claude',
|
|
63
66
|
legacyStore: new FileClaudeChannelSessionStore({
|
|
64
67
|
resolvePath: (agentId) => resolveClaudeSessionMapPath(config.stateRootDir, agentId),
|
|
@@ -69,10 +72,11 @@ function createClaudeProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
|
69
72
|
}), config.resolveStableAgentId, debugLogger);
|
|
70
73
|
return new ClaudeProviderAdapter(cli, turnPreparer);
|
|
71
74
|
}
|
|
72
|
-
function createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, implementation, turnPreparer, policyAuditGateEnabled, authorizationAuditSink) {
|
|
75
|
+
function createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, implementation, turnPreparer, policyAuditGateEnabled, authorizationAuditSink, idleBackendShutdownMs) {
|
|
73
76
|
const policyMode = resolveInternalPolicyMode(policyAuditGateEnabled);
|
|
74
77
|
const cli = new CopilotCliClient(config.copilotCommand, config.copilotArgs, {
|
|
75
78
|
idleSessionTtlMs: config.copilotSessionTtlMinutes * 60 * 1000,
|
|
79
|
+
idleBackendShutdownMs,
|
|
76
80
|
}, createProviderConnectionsSessionStore({
|
|
77
81
|
provider: 'copilot',
|
|
78
82
|
legacyStore: new FileCopilotChannelSessionStore({
|
|
@@ -90,8 +94,8 @@ function createCopilotProvider(config, debugLogger, connectionsStateGateEnabled,
|
|
|
90
94
|
? new ProviderV2CompatibilityAdapter(new CopilotProviderV2(cli), turnPreparer)
|
|
91
95
|
: new CopilotProviderAdapter(cli, turnPreparer);
|
|
92
96
|
}
|
|
93
|
-
function createCodexProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer) {
|
|
94
|
-
const cli = new CodexCliClient(config.codexCommand, config.codexArgs, {}, createProviderConnectionsSessionStore({
|
|
97
|
+
function createCodexProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer, idleBackendShutdownMs) {
|
|
98
|
+
const cli = new CodexCliClient(config.codexCommand, config.codexArgs, { idleBackendShutdownMs }, createProviderConnectionsSessionStore({
|
|
95
99
|
provider: 'codex',
|
|
96
100
|
legacyStore: new FileCodexChannelSessionStore({
|
|
97
101
|
resolvePath: (agentId) => resolveCodexSessionMapPath(config.stateRootDir, agentId),
|
|
@@ -112,6 +116,7 @@ export function createProvider(config, debugLogger, options = {}) {
|
|
|
112
116
|
const skillRuntimeGateEnabled = routingConfig.compatibilityGates.has(SKILL_RUNTIME_COMPATIBILITY_GATE);
|
|
113
117
|
const localhostGatewayGateEnabled = routingConfig.compatibilityGates.has(LOCALHOST_GATEWAY_COMPATIBILITY_GATE);
|
|
114
118
|
const policyAuditGateEnabled = routingConfig.compatibilityGates.has(POLICY_AUDIT_ENFORCEMENT_COMPATIBILITY_GATE);
|
|
119
|
+
const idleBackendShutdownMs = resolveIdleBackendShutdownMs(config.providerIdleShutdownMinutes, routingConfig.compatibilityGates);
|
|
115
120
|
const channelContextStore = contextInjectionGateEnabled
|
|
116
121
|
? new FileChannelContextStore(config.stateRootDir, {
|
|
117
122
|
skillRuntimeEnabled: skillRuntimeGateEnabled,
|
|
@@ -124,16 +129,16 @@ export function createProvider(config, debugLogger, options = {}) {
|
|
|
124
129
|
const turnPreparer = new ProviderTurnPreparer(channelContextStore, debugLogger);
|
|
125
130
|
switch (config.provider) {
|
|
126
131
|
case 'claude': {
|
|
127
|
-
return createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer);
|
|
132
|
+
return createClaudeProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer, idleBackendShutdownMs);
|
|
128
133
|
}
|
|
129
134
|
case 'codex': {
|
|
130
135
|
if (!routingConfig.compatibilityGates.has(CODEX_PROVIDER_COMPATIBILITY_GATE)) {
|
|
131
136
|
throw new Error(`Unsupported provider: ${String(config.provider)} (enable ${CODEX_PROVIDER_COMPATIBILITY_GATE})`);
|
|
132
137
|
}
|
|
133
|
-
return createCodexProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer);
|
|
138
|
+
return createCodexProvider(config, debugLogger, connectionsStateGateEnabled, turnPreparer, idleBackendShutdownMs);
|
|
134
139
|
}
|
|
135
140
|
case 'copilot': {
|
|
136
|
-
return createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, resolveCopilotImplementation(routingConfig), turnPreparer, policyAuditGateEnabled, options.authorizationAuditSink);
|
|
141
|
+
return createCopilotProvider(config, debugLogger, connectionsStateGateEnabled, resolveCopilotImplementation(routingConfig), turnPreparer, policyAuditGateEnabled, options.authorizationAuditSink, idleBackendShutdownMs);
|
|
137
142
|
}
|
|
138
143
|
default:
|
|
139
144
|
throw new Error(`Unsupported provider: ${String(config.provider)}`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const IDLE_BACKEND_SHUTDOWN_DISABLED_MS = 0;
|
|
2
|
+
export declare function resolveIdleBackendShutdownMs(idleShutdownMinutes: number, compatibilityGates: ReadonlySet<string>): number;
|
|
3
|
+
export interface IdleBackendShutdownSchedulerOptions {
|
|
4
|
+
idleShutdownMs: number;
|
|
5
|
+
isIdle: () => boolean;
|
|
6
|
+
shutdown: () => Promise<void>;
|
|
7
|
+
onShutdownFailed: (error: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare class IdleBackendShutdownScheduler {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private timer?;
|
|
12
|
+
constructor(options: IdleBackendShutdownSchedulerOptions);
|
|
13
|
+
get enabled(): boolean;
|
|
14
|
+
cancel(): void;
|
|
15
|
+
reconcile(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle shutdown policy for a provider's shared ACP adapter process.
|
|
3
|
+
*
|
|
4
|
+
* Every agent keeps one adapter child process resident across turns, so an
|
|
5
|
+
* agent nobody is talking to still pins a whole coding-CLI runtime in memory.
|
|
6
|
+
* The scheduler arms a timer whenever its owning client reports that no channel
|
|
7
|
+
* has work left, and hands the teardown back to the client once that timer
|
|
8
|
+
* expires; the client re-spawns the process lazily on the next turn.
|
|
9
|
+
*
|
|
10
|
+
* The `isIdle` probe is re-evaluated when the timer fires because a turn can
|
|
11
|
+
* arrive between arming and expiry, and the timer callback is the last point at
|
|
12
|
+
* which the shutdown can still be abandoned cheaply.
|
|
13
|
+
*/
|
|
14
|
+
import { PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE } from '../compatibility-gates.js';
|
|
15
|
+
export const IDLE_BACKEND_SHUTDOWN_DISABLED_MS = 0;
|
|
16
|
+
export function resolveIdleBackendShutdownMs(idleShutdownMinutes, compatibilityGates) {
|
|
17
|
+
if (!compatibilityGates.has(PROVIDER_IDLE_SHUTDOWN_COMPATIBILITY_GATE)) {
|
|
18
|
+
return IDLE_BACKEND_SHUTDOWN_DISABLED_MS;
|
|
19
|
+
}
|
|
20
|
+
return idleShutdownMinutes * 60 * 1000;
|
|
21
|
+
}
|
|
22
|
+
export class IdleBackendShutdownScheduler {
|
|
23
|
+
options;
|
|
24
|
+
timer;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
get enabled() {
|
|
29
|
+
return this.options.idleShutdownMs > IDLE_BACKEND_SHUTDOWN_DISABLED_MS;
|
|
30
|
+
}
|
|
31
|
+
cancel() {
|
|
32
|
+
if (!this.timer) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
clearTimeout(this.timer);
|
|
36
|
+
this.timer = undefined;
|
|
37
|
+
}
|
|
38
|
+
reconcile() {
|
|
39
|
+
this.cancel();
|
|
40
|
+
if (!this.enabled || !this.options.isIdle()) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.timer = setTimeout(() => {
|
|
44
|
+
this.timer = undefined;
|
|
45
|
+
if (!this.options.isIdle()) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
void this.options.shutdown().catch((error) => {
|
|
49
|
+
this.options.onShutdownFailed(error);
|
|
50
|
+
});
|
|
51
|
+
}, this.options.idleShutdownMs);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ProviderCommandConfig {
|
|
|
7
7
|
copilotCommand: string;
|
|
8
8
|
copilotArgs: string[];
|
|
9
9
|
copilotSessionTtlMinutes: number;
|
|
10
|
+
/** Minutes a provider's shared ACP adapter process may stay idle before it is shut down; `0` keeps it resident. */
|
|
11
|
+
providerIdleShutdownMinutes: number;
|
|
10
12
|
}
|
|
11
13
|
export interface ProviderRuntimeConfig extends ProviderCommandConfig {
|
|
12
14
|
provider: ProviderKind;
|
|
@@ -342,6 +344,11 @@ export interface ProviderReply {
|
|
|
342
344
|
awaitingUser?: ProviderAwaitingUser;
|
|
343
345
|
control?: ProviderTurnControl;
|
|
344
346
|
controlMalformed?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* The provider session this turn ran in, when the provider exposes one.
|
|
349
|
+
* agents-host records it on the task as agent.session_id.
|
|
350
|
+
*/
|
|
351
|
+
sessionId?: string;
|
|
345
352
|
}
|
|
346
353
|
export interface PostedMessage {
|
|
347
354
|
messageId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgee/agents-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.35",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"tsx": "^4.20.5",
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"vitest": "^4.1.5",
|
|
38
|
-
"@borgee/plugin-sdk": "0.
|
|
38
|
+
"@borgee/plugin-sdk": "0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"predev": "pnpm --filter @borgee/plugin-sdk build",
|
|
@@ -25,11 +25,28 @@ Use one of the packaged local CLIs to inspect the current channel bootstrap payl
|
|
|
25
25
|
|
|
26
26
|
The same CLIs also expose the task commands. Which ones apply depends on where the turn runs, which the injected `context.json` reports through `taskAssignmentContext`:
|
|
27
27
|
|
|
28
|
-
- Parent channel (no `taskAssignmentContext.active`): `--create-task --title ...`, `--list-tasks`, `--get-task --task-id ...`, `--update-task --task-id ...`, `--read-task-history --task-id ...`
|
|
29
|
-
- Task-assignment thread (`taskAssignmentContext.active: true`): `--get-task` and `--
|
|
28
|
+
- Parent channel (no `taskAssignmentContext.active`): `--create-task --title ...`, `--list-tasks`, `--get-task --task-id ...`, `--update-task --task-id ...`, `--read-task-history --task-id ...`, `--set-property <key>=<value> --task-id ...`, `--delete-property <key> --task-id ...`
|
|
29
|
+
- Task-assignment thread (`taskAssignmentContext.active: true`): `--get-task`, `--update-task`, `--set-property` and `--delete-property` may omit `--task-id` and resolve the current thread task through the persisted `currentTaskId` or an agents-host local fallback; `--read-task-history --task-id ...` still works for that thread's own task, while `--create-task` and `--list-tasks` stay disabled and must be run from the parent channel
|
|
30
30
|
|
|
31
31
|
`--read-task-history` reads the messages inside a task's thread and always requires an explicit `--task-id`; inside that task's own thread `--read-history` already reads the same messages, so the turn prompt offers the command in the parent channel only. It accepts the same `--limit` / `--before` / `--after` window as `--read-history` and answers the same not-found error for a task outside the current channel as for a task that does not exist.
|
|
32
32
|
|
|
33
|
+
## Task properties
|
|
34
|
+
|
|
35
|
+
A task property associates a task with something outside it. Use them to record what a reader would otherwise have to hunt for in the thread — the PR that implements the task, the issue it came from. Read them back with `--get-task`: every task response carries a `properties` object (`{}` when it has none).
|
|
36
|
+
|
|
37
|
+
- Set: `node ./borgee-agent.mjs --context ... --auth-path ... --set-property "link.pr=https://github.com/org/repo/pull/12"`
|
|
38
|
+
- Delete: `node ./borgee-agent.mjs --context ... --auth-path ... --delete-property "link.pr"`
|
|
39
|
+
|
|
40
|
+
Pass `key=value` as ONE shell token so a value containing `=` survives intact. Each call writes exactly one key, which is what makes it safe to write a property while another agent is writing a different one on the same task — so never try to set several at once, just call again.
|
|
41
|
+
|
|
42
|
+
Registered keys:
|
|
43
|
+
|
|
44
|
+
- `link.pr` — the pull request implementing this task. Set it as soon as the PR exists, not at the end.
|
|
45
|
+
- `link.issue` — the issue or ticket the task originates from.
|
|
46
|
+
- `agent.session_id` — **do not write this**. agents-host records the provider session itself after each turn.
|
|
47
|
+
|
|
48
|
+
An unregistered key is rejected; a value is a plain string capped at 8 KiB. Properties are references, not notes — anything that needs prose belongs in the task description or a thread message.
|
|
49
|
+
|
|
33
50
|
The private draft snapshot is read-only, collaboration-scoped, and separate from ordinary public channel messages.
|
|
34
51
|
Auxiliary sends are only for short targeted escalation, reply-thread nudges, or mentions. They must not be used for the main final answer body, which still belongs to AgentsHost.
|
|
35
52
|
Both CLIs are local-only, may only access the loopback gateway described above, and must not mutate files.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
|
|
5
5
|
const TASK_THREAD_COLLECTION_COMMAND_ERROR
|
|
6
|
-
= 'Task assignment threads only support --get-task and --
|
|
6
|
+
= 'Task assignment threads only support --get-task, --update-task, --set-property and --delete-property. Create/list tasks belong to the parent channel.';
|
|
7
7
|
const TASK_THREAD_MISSING_TASK_ID_ERROR
|
|
8
8
|
= 'Task assignment thread could not resolve the current task from persisted context or local fallback. Pass --task-id explicitly.';
|
|
9
9
|
// A window value outside the IEEE-754 safe integer range does not survive this CLI: it parses
|
|
@@ -28,6 +28,8 @@ function parseArgs(argv) {
|
|
|
28
28
|
let assigneeId;
|
|
29
29
|
let taskId;
|
|
30
30
|
let status;
|
|
31
|
+
let propertyKey;
|
|
32
|
+
let propertyValue;
|
|
31
33
|
const mentions = [];
|
|
32
34
|
|
|
33
35
|
function setAction(nextAction) {
|
|
@@ -126,6 +128,32 @@ function parseArgs(argv) {
|
|
|
126
128
|
setAction('update-task');
|
|
127
129
|
continue;
|
|
128
130
|
}
|
|
131
|
+
if (arg === '--set-property') {
|
|
132
|
+
setAction('set-property');
|
|
133
|
+
// key=value in one token so the pair cannot be split across flags and
|
|
134
|
+
// land half-applied.
|
|
135
|
+
const pair = argv[index + 1];
|
|
136
|
+
if (pair == null || !pair.includes('=')) {
|
|
137
|
+
throw new Error('--set-property requires <key>=<value>');
|
|
138
|
+
}
|
|
139
|
+
const separator = pair.indexOf('=');
|
|
140
|
+
propertyKey = pair.slice(0, separator);
|
|
141
|
+
propertyValue = pair.slice(separator + 1);
|
|
142
|
+
if (propertyKey.trim().length === 0) {
|
|
143
|
+
throw new Error('--set-property requires a non-empty key');
|
|
144
|
+
}
|
|
145
|
+
index += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === '--delete-property') {
|
|
149
|
+
setAction('delete-property');
|
|
150
|
+
propertyKey = argv[index + 1];
|
|
151
|
+
if (propertyKey == null || propertyKey.trim().length === 0) {
|
|
152
|
+
throw new Error('--delete-property requires a key');
|
|
153
|
+
}
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
129
157
|
if (arg === '--limit') {
|
|
130
158
|
limit = parseIntegerOption(arg, argv[index + 1]);
|
|
131
159
|
index += 1;
|
|
@@ -209,6 +237,8 @@ function parseArgs(argv) {
|
|
|
209
237
|
assigneeId,
|
|
210
238
|
taskId,
|
|
211
239
|
status,
|
|
240
|
+
propertyKey: propertyKey?.trim(),
|
|
241
|
+
propertyValue,
|
|
212
242
|
};
|
|
213
243
|
}
|
|
214
244
|
|
|
@@ -370,6 +400,27 @@ function resolveGatewayRequest(payload, action, options) {
|
|
|
370
400
|
usesCurrentThreadFallback: false,
|
|
371
401
|
};
|
|
372
402
|
}
|
|
403
|
+
case 'set-property':
|
|
404
|
+
case 'delete-property': {
|
|
405
|
+
const taskId = resolveTaskId(payload, action, options);
|
|
406
|
+
const method = action === 'set-property' ? 'PUT' : 'DELETE';
|
|
407
|
+
const requestBody = action === 'set-property' ? { value: options.propertyValue } : undefined;
|
|
408
|
+
const encodedKey = encodeURIComponent(String(options.propertyKey ?? ''));
|
|
409
|
+
if (payload.taskAssignmentContext?.active === true && explicitTaskId(options) === null) {
|
|
410
|
+
return {
|
|
411
|
+
url: new URL(`/v1/channels/${encodedChannelId}/current-task/properties/${encodedKey}`, gateway.baseUrl),
|
|
412
|
+
method,
|
|
413
|
+
body: requestBody,
|
|
414
|
+
usesCurrentThreadFallback: true,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
url: new URL(`/v1/tasks/${encodeURIComponent(taskId)}/properties/${encodedKey}`, gateway.baseUrl),
|
|
419
|
+
method,
|
|
420
|
+
body: requestBody,
|
|
421
|
+
usesCurrentThreadFallback: false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
373
424
|
default:
|
|
374
425
|
throw new Error(`Unsupported gateway action: ${action}`);
|
|
375
426
|
}
|
|
@@ -480,6 +531,8 @@ const {
|
|
|
480
531
|
assigneeId,
|
|
481
532
|
taskId,
|
|
482
533
|
status,
|
|
534
|
+
propertyKey,
|
|
535
|
+
propertyValue,
|
|
483
536
|
} = parseArgs(process.argv.slice(2));
|
|
484
537
|
const payload = JSON.parse(await readFile(contextPath, 'utf8'));
|
|
485
538
|
|
|
@@ -502,6 +555,8 @@ if (action === 'print-bootstrap') {
|
|
|
502
555
|
assigneeId,
|
|
503
556
|
taskId,
|
|
504
557
|
status,
|
|
558
|
+
propertyKey,
|
|
559
|
+
propertyValue,
|
|
505
560
|
});
|
|
506
561
|
console.log(JSON.stringify(result, null, 2));
|
|
507
562
|
}
|
|
@@ -11,8 +11,8 @@ from urllib.parse import quote, urlencode
|
|
|
11
11
|
from urllib.request import Request, urlopen
|
|
12
12
|
|
|
13
13
|
TASK_THREAD_COLLECTION_COMMAND_ERROR = (
|
|
14
|
-
"Task assignment threads only support --get-task
|
|
15
|
-
"Create/list tasks belong to the parent channel."
|
|
14
|
+
"Task assignment threads only support --get-task, --update-task, --set-property "
|
|
15
|
+
"and --delete-property. Create/list tasks belong to the parent channel."
|
|
16
16
|
)
|
|
17
17
|
TASK_THREAD_MISSING_TASK_ID_ERROR = (
|
|
18
18
|
"Task assignment thread could not resolve the current task from persisted context "
|
|
@@ -54,6 +54,8 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
54
54
|
assignee_id: str | None = None
|
|
55
55
|
task_id: str | None = None
|
|
56
56
|
status: str | None = None
|
|
57
|
+
property_key: str | None = None
|
|
58
|
+
property_value: str | None = None
|
|
57
59
|
index = 0
|
|
58
60
|
|
|
59
61
|
def set_action(next_action: str) -> None:
|
|
@@ -155,6 +157,23 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
155
157
|
elif arg == "--status":
|
|
156
158
|
index += 1
|
|
157
159
|
status = argv[index] if index < len(argv) else None
|
|
160
|
+
elif arg == "--set-property":
|
|
161
|
+
set_action("set-property")
|
|
162
|
+
index += 1
|
|
163
|
+
# key=value in one token so the pair cannot be split across flags
|
|
164
|
+
# and land half-applied.
|
|
165
|
+
pair = argv[index] if index < len(argv) else None
|
|
166
|
+
if pair is None or "=" not in pair:
|
|
167
|
+
raise ValueError("--set-property requires <key>=<value>")
|
|
168
|
+
property_key, property_value = pair.split("=", 1)
|
|
169
|
+
if not property_key.strip():
|
|
170
|
+
raise ValueError("--set-property requires a non-empty key")
|
|
171
|
+
elif arg == "--delete-property":
|
|
172
|
+
set_action("delete-property")
|
|
173
|
+
index += 1
|
|
174
|
+
property_key = argv[index] if index < len(argv) else None
|
|
175
|
+
if property_key is None or not property_key.strip():
|
|
176
|
+
raise ValueError("--delete-property requires a key")
|
|
158
177
|
else:
|
|
159
178
|
raise ValueError(f"Unknown argument: {arg}")
|
|
160
179
|
index += 1
|
|
@@ -178,6 +197,8 @@ def parse_args(argv: list[str]) -> dict[str, object]:
|
|
|
178
197
|
"assignee_id": assignee_id,
|
|
179
198
|
"task_id": task_id,
|
|
180
199
|
"status": status,
|
|
200
|
+
"property_key": property_key.strip() if isinstance(property_key, str) else None,
|
|
201
|
+
"property_value": property_value,
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
|
|
@@ -303,6 +324,14 @@ def resolve_gateway_request(
|
|
|
303
324
|
if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
|
|
304
325
|
return f"{base_url}/v1/channels/{channel_id}/current-task", "PATCH", request_body, True
|
|
305
326
|
return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}", "PATCH", request_body, False
|
|
327
|
+
if action in ("set-property", "delete-property"):
|
|
328
|
+
task_id = resolve_task_id(payload, action, options)
|
|
329
|
+
method = "PUT" if action == "set-property" else "DELETE"
|
|
330
|
+
request_body = {"value": options.get("property_value")} if action == "set-property" else None
|
|
331
|
+
encoded_key = quote(str(options.get("property_key") or ""), safe="")
|
|
332
|
+
if isinstance(task_assignment_context, dict) and task_assignment_context.get("active") is True and explicit_task_id(options) is None:
|
|
333
|
+
return f"{base_url}/v1/channels/{channel_id}/current-task/properties/{encoded_key}", method, request_body, True
|
|
334
|
+
return f"{base_url}/v1/tasks/{quote(str(task_id), safe='')}/properties/{encoded_key}", method, request_body, False
|
|
306
335
|
raise ValueError(f"Unsupported gateway action: {action}")
|
|
307
336
|
|
|
308
337
|
|
|
@@ -431,6 +460,8 @@ else:
|
|
|
431
460
|
"assignee_id": parsed["assignee_id"],
|
|
432
461
|
"task_id": parsed["task_id"],
|
|
433
462
|
"status": parsed["status"],
|
|
463
|
+
"property_key": parsed["property_key"],
|
|
464
|
+
"property_value": parsed["property_value"],
|
|
434
465
|
},
|
|
435
466
|
),
|
|
436
467
|
indent=2,
|