agentlinkops 0.6.8 → 0.6.10
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +44 -12
- package/cli/admission.js +9 -1
- package/cli/agent-setup.js +189 -20
- package/cli/catalog.js +7938 -1357
- package/cli/client.js +89 -23
- package/cli/cloud-error.js +21 -0
- package/cli/commands.js +37 -6
- package/cli/connection.js +74 -12
- package/cli/doctor.js +24 -9
- package/cli/gsc-browser.js +19 -6
- package/cli/ledger.js +4 -7
- package/cli/lifecycle.js +5 -2
- package/cli/local-result.js +4 -4
- package/cli/main.js +43 -28
- package/cli/mcp-client.js +198 -0
- package/cli/native-lock.js +22 -0
- package/cli/oauth-credentials.js +196 -0
- package/cli/oauth-flow.js +259 -0
- package/cli/setup-plan.js +5 -5
- package/cli/status.js +14 -2
- package/cli/sync-lock.js +105 -0
- package/cli/sync.js +14 -6
- package/package.json +6 -3
- package/references/cli.md +22 -0
- package/references/local-crm-draft-handoff.md +76 -0
- package/references/portable-ledger-handoff.md +50 -0
- package/references/recipes/catalog.json +9 -9
- package/references/recipes/command-notes.md +17 -1
- package/references/recipes/placement-reconciliation.md +19 -5
- package/references/recipes/qualified-campaign-handoff.md +16 -4
- package/references/recipes/site-context-brief.md +2 -2
- package/references/recipes/sourced-linkable-asset.md +8 -8
- package/scripts/agentlinkops.py +30 -5
- package/scripts/outreach-crm-draft.py +121 -0
- package/scripts/portable-handoff.py +184 -0
- package/shared/exec-grammar.js +342 -0
- package/shared/lifecycle-contract.js +2 -2
- package/shared/response-contract.js +7 -1
- package/skills/agentlinkops-assets/SKILL.md +2 -2
- package/skills/agentlinkops-campaigns/SKILL.md +3 -3
- package/skills/agentlinkops-connect/SKILL.md +29 -6
- package/skills/agentlinkops-connect/references/agentlinkops.md +62 -26
- package/skills/agentlinkops-crm/SKILL.md +17 -3
- package/src/citations/adapter.js +3 -2
- package/src/citations/browser/chatgpt-citations.js +60 -6
- package/src/citations/browser/engine.js +106 -18
- package/src/citations/browser/shims/utils.js +9 -2
- package/src/citations/login.js +42 -5
- package/src/discovery/contract.js +1 -1
- package/src/response-shaping.js +58 -0
- package/src/tool-views.js +31 -4
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
export {
|
|
5
5
|
PROVIDER_NO_OUTPUT_TIMEOUT_MS,
|
|
6
6
|
PROVIDER_FORCE_EXIT_STABLE_MS,
|
|
7
|
-
PROVIDER_EDITOR_SELECTORS,
|
|
8
7
|
PROVIDER_SUBMIT_BTN_SELECTORS,
|
|
9
8
|
PROVIDER_RESPONSE_GENERATION_SELECTORS,
|
|
10
9
|
RETRYABLE_ERRORS,
|
|
11
10
|
} from '../gen/utils/agent-constants.js';
|
|
12
|
-
import { PROVIDER_MODEL_RESPONSE_SELECTORS as vendoredResponseSelectors } from '../gen/utils/agent-constants.js';
|
|
11
|
+
import { PROVIDER_MODEL_RESPONSE_SELECTORS as vendoredResponseSelectors, PROVIDER_EDITOR_SELECTORS as vendoredEditorSelectors } from '../gen/utils/agent-constants.js';
|
|
12
|
+
|
|
13
|
+
// Anonymous composer observed 2026-09-25: a plain textarea (#mobile-composer-prompt,
|
|
14
|
+
// name="prompt"). None of the vendored ChatGPT selectors match it; without these the engine
|
|
15
|
+
// reached it only through the broad shadow-DOM fallback.
|
|
16
|
+
export const PROVIDER_EDITOR_SELECTORS = Object.freeze({
|
|
17
|
+
...vendoredEditorSelectors,
|
|
18
|
+
chatgpt: Object.freeze(['textarea#mobile-composer-prompt', 'textarea[name="prompt"]', ...vendoredEditorSelectors.chatgpt]),
|
|
19
|
+
});
|
|
13
20
|
|
|
14
21
|
// Anonymous surface observed 2026-09-20: conversation LI, distinct from its
|
|
15
22
|
// user sibling, with response actions. Never select the conversation itself.
|
package/src/citations/login.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// it's never synced to the hosted service, and `citation logout ENGINE`
|
|
9
9
|
// removes it. This is the CLI-own-browser posture — the user's account,
|
|
10
10
|
// their risk, their data.
|
|
11
|
-
import
|
|
11
|
+
import * as fs from 'node:fs/promises';
|
|
12
|
+
import { rm } from 'node:fs/promises';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
12
14
|
import { join } from 'node:path';
|
|
13
15
|
import { homedir } from 'node:os';
|
|
14
16
|
import { readFile } from 'node:fs/promises';
|
|
@@ -25,6 +27,44 @@ const ENGINE_URLS = Object.freeze({
|
|
|
25
27
|
bing: 'https://www.bing.com/',
|
|
26
28
|
});
|
|
27
29
|
|
|
30
|
+
async function saveSession(sessionDir, engineName, state, { sessionFs = fs, platform = process.platform } = {}) {
|
|
31
|
+
const fail = () => Object.assign(new Error('Session storage requires a real private directory (0700) and regular private files (0600). Choose private storage and remove symbolic links.'), { code: 'PRIVATE_STORAGE_REQUIRED' });
|
|
32
|
+
const privateMode = (info, mode) => platform === 'win32' || (info.mode & 0o7777) === mode;
|
|
33
|
+
await sessionFs.mkdir(sessionDir, { recursive: true, mode: 0o700 });
|
|
34
|
+
const directory = await sessionFs.lstat(sessionDir);
|
|
35
|
+
if (!directory.isDirectory() || !privateMode(directory, 0o700)) throw fail();
|
|
36
|
+
const sessionPath = join(sessionDir, `${engineName}.json`);
|
|
37
|
+
const inspect = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const info = await sessionFs.lstat(sessionPath);
|
|
40
|
+
if (!info.isFile() || !privateMode(info, 0o600)) throw fail();
|
|
41
|
+
return info;
|
|
42
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
43
|
+
};
|
|
44
|
+
const previous = await inspect();
|
|
45
|
+
const temporary = join(sessionDir, `.${engineName}.${randomUUID()}.tmp`);
|
|
46
|
+
let handle, owned = false;
|
|
47
|
+
try {
|
|
48
|
+
handle = await sessionFs.open(temporary, 'wx', 0o600); owned = true;
|
|
49
|
+
const info = await handle.stat();
|
|
50
|
+
if (!info.isFile() || !privateMode(info, 0o600)) throw fail();
|
|
51
|
+
await handle.writeFile(JSON.stringify(state, null, 2));
|
|
52
|
+
await handle.sync();
|
|
53
|
+
await handle.close(); handle = undefined;
|
|
54
|
+
const currentDirectory = await sessionFs.lstat(sessionDir);
|
|
55
|
+
if (!currentDirectory.isDirectory() || !privateMode(currentDirectory, 0o700) || currentDirectory.dev !== directory.dev || currentDirectory.ino !== directory.ino) throw fail();
|
|
56
|
+
const current = await inspect();
|
|
57
|
+
if (Boolean(current) !== Boolean(previous) || (current && ['dev', 'ino', 'size', 'mtimeMs'].some(key => current[key] !== previous[key]))) {
|
|
58
|
+
throw Object.assign(new Error('The saved session changed during login. Retry without another session writer.'), { code: 'SESSION_CHANGED' });
|
|
59
|
+
}
|
|
60
|
+
await sessionFs.rename(temporary, sessionPath); owned = false;
|
|
61
|
+
return sessionPath;
|
|
62
|
+
} finally {
|
|
63
|
+
await handle?.close().catch(() => {});
|
|
64
|
+
if (owned) await sessionFs.unlink(temporary).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
export async function loginEngine({ engineName, timeoutMs = 180_000, runtime = {} }) {
|
|
29
69
|
const url = ENGINE_URLS[engineName];
|
|
30
70
|
if (!url) {
|
|
@@ -80,10 +120,7 @@ export async function loginEngine({ engineName, timeoutMs = 180_000, runtime = {
|
|
|
80
120
|
// (anonymous surfaces don't need login, and the adapter handles both).
|
|
81
121
|
const state = await context.storageState();
|
|
82
122
|
const sessionDir = runtime.sessionDir ?? SESSIONS_DIR;
|
|
83
|
-
await
|
|
84
|
-
const sessionPath = join(sessionDir, `${engineName}.json`);
|
|
85
|
-
await writeFile(sessionPath, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
86
|
-
await chmod(sessionPath, 0o600);
|
|
123
|
+
const sessionPath = await saveSession(sessionDir, engineName, state, runtime);
|
|
87
124
|
|
|
88
125
|
return {
|
|
89
126
|
engine: engineName,
|
|
@@ -64,7 +64,7 @@ export const DiscoveryQuery = z.strictObject({
|
|
|
64
64
|
row_limit: count.min(1).max(DISCOVERY_LIMITS.rows),
|
|
65
65
|
}).superRefine((q, ctx) => {
|
|
66
66
|
const url = validatePublicUrl(q.target_kind === 'domain' ? `https://${q.target}/` : q.target);
|
|
67
|
-
if (!url.valid || (q.target_kind === 'domain' ? url.hostname !== q.target
|
|
67
|
+
if (!url.valid || (q.target_kind === 'domain' ? url.hostname !== q.target : url.url !== q.target))
|
|
68
68
|
ctx.addIssue({ code: 'custom', message: 'Invalid or noncanonical discovery target.', path: ['target'] });
|
|
69
69
|
if (q.target_kind === 'exact_url' && q.include_subdomains)
|
|
70
70
|
ctx.addIssue({ code: 'custom', message: 'Exact URL scope cannot include subdomains.', path: ['include_subdomains'] });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// DP-0036-T15: response shaping. A list or report command whose rows carry evidence has a
|
|
2
|
+
// reviewed CONCISE projection: the fields an agent needs to decide and to call next, with the
|
|
3
|
+
// observation facts the unknown-versus-absent rule depends on always kept (state, uncertain,
|
|
4
|
+
// the latest attempt and its reason, the last verified observation). MCP and code mode default
|
|
5
|
+
// to concise; REST and the CLI default to detailed; every caller can ask for either with
|
|
6
|
+
// `format`. Rows only are projected: top-level envelope fields (cursors, coverage, counts,
|
|
7
|
+
// reservations, run and inventory metadata) are never dropped. Rules: AGENT-SURFACE-CONTRACT.md.
|
|
8
|
+
|
|
9
|
+
export const FORMATS = ['concise','detailed'];
|
|
10
|
+
|
|
11
|
+
const pick = (row, keys) => Object.fromEntries(keys.filter(key => row[key] !== undefined).map(key => [key, row[key]]));
|
|
12
|
+
// The evidence core of an observation_state or an attempt: never more than these, never less.
|
|
13
|
+
const attempt = value => value && typeof value === 'object' ? pick(value, ['state','reason','checkedAt','httpStatus','occurrenceCount','checkerVersion']) : value;
|
|
14
|
+
export function conciseObservationState(state) {
|
|
15
|
+
if (!state || typeof state !== 'object') return state ?? {};
|
|
16
|
+
const out = pick(state, ['state','uncertain','checked_at','reason','confirmedState','lastChangedAt','firstAbsentAt','consecutiveAbsent','nextCheckAt','lastSuccessfulAt','lastHealthyAt','firstUnavailableAt','unavailableCount','retryNotBefore']);
|
|
17
|
+
if (state.latestAttempt !== undefined) out.latestAttempt = attempt(state.latestAttempt);
|
|
18
|
+
if (state.lastSuccessfulObservation !== undefined) out.lastSuccessfulObservation = attempt(state.lastSuccessfulObservation);
|
|
19
|
+
if (state.lastSuccessfulObservationSameAsLatestAttempt !== undefined) out.lastSuccessfulObservationSameAsLatestAttempt = state.lastSuccessfulObservationSameAsLatestAttempt;
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
const withState = (row, keys) => ({ ...pick(row, keys), observation_state: conciseObservationState(row.observation_state) });
|
|
23
|
+
// An event snapshot (shared/observation-event.js) minus the occurrence list, expectations and
|
|
24
|
+
// signatures: the state, uncertainty, check state, reason, status and timing stay.
|
|
25
|
+
const conciseSnapshot = snapshot => snapshot && typeof snapshot === 'object'
|
|
26
|
+
? pick(snapshot, ['state','uncertain','check_state','reason','checked_at','http_status','occurrence_count','occurrences_truncated','source_url','target_url','url','next_check_at','watch_id','target_id'])
|
|
27
|
+
: snapshot;
|
|
28
|
+
const observationResult = result => result && typeof result === 'object'
|
|
29
|
+
? { ...pick(result, ['state','reason','checkedAt','httpStatus','checkerVersion','evidence','redirects']), occurrenceCount: result.occurrenceCount ?? result.occurrences?.length ?? 0 }
|
|
30
|
+
: result;
|
|
31
|
+
|
|
32
|
+
// One entry per shaped command: which array holds the rows and how a row is projected. The
|
|
33
|
+
// `keeps` list is what the docs and the tests read; `row` is the projection itself.
|
|
34
|
+
export const SHAPES = {
|
|
35
|
+
list_rank_history: { rows: 'items', keeps: ['overview_run_id','slot_at','rank','observed_at','retrieved_at','freshness','outcome','execution','cost'],
|
|
36
|
+
row: r => pick(r, ['overview_run_id','slot_at','rank','observed_at','retrieved_at','freshness','outcome','execution','cost']) },
|
|
37
|
+
list_link_watches: { rows: 'items', keeps: ['id','source_url','target_url','status','state','observation_state','next_check_at','local_reference'], row: r => withState(r, ['id','source_url','target_url','status','state','next_check_at','local_reference']) },
|
|
38
|
+
list_targets: { rows: 'items', keeps: ['id','url','status','state','observation_state','next_check_at'], row: r => withState(r, ['id','url','status','state','next_check_at']) },
|
|
39
|
+
get_target_placements: { rows: 'items', keeps: ['id','source_url','target_url','target_scope','status','state'], row: r => pick(r, ['id','source_url','target_url','target_scope','status','state']) },
|
|
40
|
+
list_check_jobs: { rows: 'items', keeps: ['id','state','type','watch_id','target_id','attempt_count','error_code','created_at','completed_at'], row: r => pick(r, ['id','state','type','watch_id','target_id','attempt_count','error_code','created_at','completed_at']) },
|
|
41
|
+
get_history: { rows: 'items', keeps: ['id','state','reason','checked_at','job_id','evidence_key','result (state, reason, checkedAt, httpStatus, checkerVersion, evidence, redirects, occurrenceCount)'], row: r => ({ ...pick(r, ['id','state','reason','checked_at','job_id','evidence_key']), result: observationResult(r.result) }) },
|
|
42
|
+
list_events: { rows: 'events', keeps: ['id','type','watch_id','target_id','created_at','cursor','data (before and after snapshots without occurrences, expectations and signatures; observation_id; evidence_key)'], row: r => ({ ...pick(r, ['id','type','watch_id','target_id','created_at','cursor']), data: r.data && typeof r.data === 'object' ? { ...pick(r.data, ['observation_id','evidence_key']), ...(r.data.before !== undefined ? { before: conciseSnapshot(r.data.before) } : {}), ...(r.data.after !== undefined ? { after: conciseSnapshot(r.data.after) } : {}) } : r.data }) },
|
|
43
|
+
list_discovery_candidates: { rows: 'items', keeps: ['id','source_url','target_url','anchor','dofollow','provider_last_seen','verification_status','verified_at','observation_id'], row: r => pick(r, ['id','source_url','target_url','anchor','dofollow','provider_last_seen','verification_status','verified_at','observation_id']) },
|
|
44
|
+
list_competitor_inventory_rows: { rows: 'items', keeps: ['id','source_url','target_url','anchor','dofollow','provider_last_seen','verification_status','observation_id','member_role'], row: r => pick(r, ['id','source_url','target_url','anchor','dofollow','provider_last_seen','verification_status','observation_id','member_role']) },
|
|
45
|
+
get_candidate_verification_batch: { rows: 'items', keeps: ['ordinal','candidate_id','local_reference','job_id','watch_id','state','error_code','observation','usage'], row: r => pick(r, ['ordinal','candidate_id','local_reference','job_id','watch_id','state','error_code','observation','usage']) },
|
|
46
|
+
};
|
|
47
|
+
export const SHAPED_COMMANDS = Object.keys(SHAPES);
|
|
48
|
+
|
|
49
|
+
// Apply the requested (or defaulted) format. Detailed returns the data untouched apart from
|
|
50
|
+
// `defaults_applied`; concise projects the rows. Both formats say which defaults were applied.
|
|
51
|
+
export function shapeResponse(name, data, { format = null, defaultFormat = 'detailed' } = {}) {
|
|
52
|
+
const shape = SHAPES[name];
|
|
53
|
+
if (!shape || !data || typeof data !== 'object' || Array.isArray(data)) return data;
|
|
54
|
+
const applied = format ?? defaultFormat;
|
|
55
|
+
const defaults = format ? {} : { format: defaultFormat };
|
|
56
|
+
if (applied !== 'concise' || !Array.isArray(data[shape.rows])) return { ...data, defaults_applied: defaults };
|
|
57
|
+
return { ...data, [shape.rows]: data[shape.rows].map(row => row && typeof row === 'object' ? shape.row(row) : row), defaults_applied: defaults };
|
|
58
|
+
}
|
package/src/tool-views.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// script, the MCP router, the CLI index and the generators all read the same definitions.
|
|
4
4
|
// Rules: docs/technical/AGENT-SURFACE-CONTRACT.md (DP-0036).
|
|
5
5
|
|
|
6
|
-
export const TOOLSETS = ['monitoring','evidence','discovery','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','digests'];
|
|
6
|
+
export const TOOLSETS = ['monitoring','evidence','rank','discovery','baseline','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','digests'];
|
|
7
7
|
|
|
8
8
|
// One line per toolset: what the group is for. Printed by the site brief, the REST index and the
|
|
9
9
|
// catalog snapshot so the same wording reaches every client; a toolset without a summary is a
|
|
@@ -15,7 +15,9 @@ export const TOOLSET_SUMMARIES = {
|
|
|
15
15
|
digests: 'Own-address digest preferences, delivery history and exact events.',
|
|
16
16
|
monitoring: 'Watch earned links and destination URLs: create, list, update, import, export, recheck.',
|
|
17
17
|
evidence: 'What a check observed: histories, snapshots, change feeds, check jobs, published contacts.',
|
|
18
|
+
rank: 'Synthetic DataForSEO rank schedules and dated history; hosted collection disabled.',
|
|
18
19
|
discovery: 'Import candidate rows, read stored runs, verify selected candidates, enroll them.',
|
|
20
|
+
baseline: 'Scoped backlink baselines; supplier admission disabled.',
|
|
19
21
|
library: 'Read and export the coverage-stated opportunity library.',
|
|
20
22
|
competitors: 'Competitor sets, dated inventories, scheduled refresh, gap and domain-mix reports.',
|
|
21
23
|
reports: 'Profile, anchor and report summaries over the tracked dataset.',
|
|
@@ -58,7 +60,7 @@ export function oneLiner(description, max = 60) {
|
|
|
58
60
|
export function manifestText(entries, { max = 36 } = {}) {
|
|
59
61
|
const groups = new Map(TOOLSETS.map(t => [t, []]));
|
|
60
62
|
for (const entry of entries) (groups.get(entry.toolset) ?? groups.set(entry.toolset, []).get(entry.toolset)).push(entry);
|
|
61
|
-
const lines = ['Commands by toolset (name: what it does).
|
|
63
|
+
const lines = ['Commands by toolset (name: what it does). On /mcp use exec: `exec search <words>`, `exec describe <name>`, `exec call <name> <json>`; the CLI mirrors the verbs as `agentlinkops tools | describe | call`.'];
|
|
62
64
|
for (const [toolset, members] of groups) {
|
|
63
65
|
if (!members.length) continue;
|
|
64
66
|
lines.push(`\n${toolset}:`);
|
|
@@ -94,11 +96,34 @@ export const CODE_TOOLS = [
|
|
|
94
96
|
];
|
|
95
97
|
export const CODE_LIMITS = { outputChars:24000, timeoutMs:30000, maxCalls:200 };
|
|
96
98
|
|
|
99
|
+
// DP-0059: the default view's single exec tool. One definition carries the whole registry: the
|
|
100
|
+
// agent searches, inspects and calls commands through CLI-style strings instead of loading
|
|
101
|
+
// schemas upfront (the pattern PostHog's MCP shipped and we adopted). The definition lives here
|
|
102
|
+
// beside META_TOOLS and CODE_TOOLS so the measurement script counts the real shape the router
|
|
103
|
+
// registers; the four meta tools remain registered on /mcp for direct calls by clients that
|
|
104
|
+
// learned them, unlisted.
|
|
105
|
+
export const EXEC_TOOL = {
|
|
106
|
+
name: 'exec',
|
|
107
|
+
description: 'Run any AgentLinkOps command through one CLI-style command string: search finds commands by task, describe shows an exact schema, call runs it (reads and writes, under this connection\'s scopes). Find with search, describe once, then call and reuse the schema; never guess a schema. One command per call; several exec calls can run in parallel.',
|
|
108
|
+
readOnly: false,
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
command: {
|
|
113
|
+
type: 'string', minLength: 1, maxLength: 8000,
|
|
114
|
+
description: 'CLI-style command. Verbs: search [--toolset T] [--limit N] <words>; tools [toolset]; describe [--output-schema] [--examples] <name>...; schema <name> [<field.path>]; call [--json] <name> <json_input>. --json returns full rows instead of the concise projection. A schema larger than the context budget is summarized; drill any field carrying a hint with schema <name> <field.path>. Unknown name: run search <words>.',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ['command'],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
97
122
|
// Views. `entries` carry name, toolset, tier, readOnly and (from tool-exposure.js) unavailable.
|
|
98
123
|
// An unavailable command stays a member of its views: it is listed with its marker, never
|
|
99
124
|
// hidden, so a client that calls it gets the stable configuration error rather than not-found.
|
|
100
125
|
export const VIEW_BUDGETS = {
|
|
101
|
-
default: { definitions:
|
|
126
|
+
default: { definitions:1, tokens:3500 },
|
|
102
127
|
toolset: { definitions:22, tokens:8000 },
|
|
103
128
|
code: { definitions:3, tokens:2000 },
|
|
104
129
|
manifest: { tokens:2500 },
|
|
@@ -108,7 +133,9 @@ export function viewMembers(entries, view) {
|
|
|
108
133
|
const core = visible.filter(e => e.tier === 'core');
|
|
109
134
|
if (view === 'all') return visible;
|
|
110
135
|
if (view === 'readonly') return visible.filter(e => e.readOnly);
|
|
111
|
-
|
|
136
|
+
// DP-0059: the default view lists exactly one tool, exec. Every command (core tier included)
|
|
137
|
+
// stays registered and callable there through exec call and by a direct tools/call.
|
|
138
|
+
if (view === 'default') return [];
|
|
112
139
|
if (view === 'code') return [];
|
|
113
140
|
const m = /^toolset:([a-z]+)(:readonly)?$/.exec(view);
|
|
114
141
|
if (!m) throw new Error(`Unknown view ${view}`);
|