aptly 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +45 -0
  3. package/README.ja.md +45 -0
  4. package/README.md +45 -0
  5. package/README.zh-CN.md +45 -0
  6. package/dist/bin/aptly.d.ts +2 -0
  7. package/dist/bin/aptly.js +122 -0
  8. package/dist/src/automatic-hook.d.ts +42 -0
  9. package/dist/src/automatic-hook.js +232 -0
  10. package/dist/src/catalog.d.ts +19 -0
  11. package/dist/src/catalog.js +135 -0
  12. package/dist/src/config.d.ts +24 -0
  13. package/dist/src/config.js +75 -0
  14. package/dist/src/core.d.ts +50 -0
  15. package/dist/src/core.js +324 -0
  16. package/dist/src/diagnostics.d.ts +11 -0
  17. package/dist/src/diagnostics.js +68 -0
  18. package/dist/src/files.d.ts +10 -0
  19. package/dist/src/files.js +70 -0
  20. package/dist/src/hook.d.ts +20 -0
  21. package/dist/src/hook.js +102 -0
  22. package/dist/src/inspect.d.ts +31 -0
  23. package/dist/src/inspect.js +219 -0
  24. package/dist/src/model-info.d.ts +32 -0
  25. package/dist/src/model-info.js +223 -0
  26. package/dist/src/provenance.d.ts +62 -0
  27. package/dist/src/provenance.js +266 -0
  28. package/dist/src/provider.d.ts +9 -0
  29. package/dist/src/provider.js +80 -0
  30. package/dist/src/report.d.ts +51 -0
  31. package/dist/src/report.js +177 -0
  32. package/dist/src/runtime.d.ts +8 -0
  33. package/dist/src/runtime.js +22 -0
  34. package/dist/src/server.d.ts +7 -0
  35. package/dist/src/server.js +80 -0
  36. package/dist/src/setup.d.ts +22 -0
  37. package/dist/src/setup.js +408 -0
  38. package/dist/src/types.d.ts +76 -0
  39. package/dist/src/types.js +2 -0
  40. package/dist/src/version.d.ts +1 -0
  41. package/dist/src/version.js +1 -0
  42. package/docs/ADVISORY_USAGE.md +196 -0
  43. package/docs/DRAFT_0_1_0.md +55 -0
  44. package/docs/guide/en.md +190 -0
  45. package/docs/guide/ja.md +190 -0
  46. package/docs/guide/ko.md +190 -0
  47. package/docs/guide/zh-CN.md +190 -0
  48. package/examples/claude-mcp.json +15 -0
  49. package/examples/codex-mcp.toml +9 -0
  50. package/examples/recommend.claude.json +9 -0
  51. package/examples/recommend.codex.json +9 -0
  52. package/examples/router.shadow.json +5 -0
  53. package/examples/settings-inventory.json +5 -0
  54. package/package.json +75 -0
@@ -0,0 +1,51 @@
1
+ declare const statuses: readonly ['ok', 'warning', 'error', 'unknown'];
2
+ export type DiagnosticStatus = typeof statuses[number];
3
+ declare const checkCodes: Readonly<{
4
+ node_runtime: string[];
5
+ classification_provider: string[];
6
+ api_key: string[];
7
+ ledger_location: string[];
8
+ operator_configuration: string[];
9
+ host_evidence: string[];
10
+ host_cli_versions: string[];
11
+ }>;
12
+ declare const privacyKeys: readonly ['task_text', 'paths', 'environment_values', 'credentials', 'raw_errors'];
13
+ export type DiagnosticCheckId = keyof typeof checkCodes;
14
+ export interface DiagnosticCheck {
15
+ id: DiagnosticCheckId;
16
+ status: DiagnosticStatus;
17
+ code: string;
18
+ }
19
+ export type DiagnosticSummary = Record<DiagnosticStatus, number>;
20
+ export interface Diagnostic {
21
+ schema_version: 1;
22
+ report_id: string;
23
+ generated_at: string;
24
+ tool: {
25
+ name: 'aptly' | 'subagent-decisions';
26
+ version: string;
27
+ };
28
+ environment: {
29
+ node: string;
30
+ platform: string;
31
+ arch: string;
32
+ };
33
+ checks: DiagnosticCheck[];
34
+ summary: DiagnosticSummary;
35
+ privacy: Record<typeof privacyKeys[number], false>;
36
+ network_requests: 0;
37
+ paid_calls: 0;
38
+ }
39
+ export interface ReportExportResult {
40
+ report_id: string;
41
+ files: string[];
42
+ summary: DiagnosticSummary;
43
+ }
44
+ export declare function renderDiagnosticMarkdown(diagnostic: unknown): string;
45
+ /** Write reviewed, local attachments only. No network, process execution, or config changes. */
46
+ export declare function exportDiagnosticReport({ diagnostic, format, outputDir }?: {
47
+ diagnostic?: unknown;
48
+ format?: unknown;
49
+ outputDir?: unknown;
50
+ }): ReportExportResult;
51
+ export {};
@@ -0,0 +1,177 @@
1
+ import { closeSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, rmdirSync, unlinkSync, writeFileSync, chmodSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { assertNoRedirectedAncestors } from './files.js';
4
+ import { errorCode } from './types.js';
5
+ const statuses = ['ok', 'warning', 'error', 'unknown'];
6
+ const checkCodes = Object.freeze({
7
+ node_runtime: ['node_supported', 'node_unsupported'],
8
+ classification_provider: ['provider_fixture', 'provider_decisions', 'provider_invalid'],
9
+ api_key: ['key_present_unverified', 'key_missing', 'key_not_required'],
10
+ ledger_location: ['ledger_private_location_ready', 'ledger_not_configured', 'ledger_location_unavailable'],
11
+ operator_configuration: ['configuration_default_shadow', 'configuration_valid', 'configuration_unavailable', 'configuration_invalid'],
12
+ host_evidence: ['host_evidence_not_configured', 'host_evidence_loaded_unverified', 'host_evidence_unavailable', 'host_evidence_changed'],
13
+ host_cli_versions: ['host_cli_not_probed'],
14
+ });
15
+ const privacyKeys = ['task_text', 'paths', 'environment_values', 'credentials', 'raw_errors'];
16
+ const platforms = ['aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'netbsd', 'openbsd', 'sunos', 'win32', 'unknown'];
17
+ const architectures = ['arm', 'arm64', 'ia32', 'loong64', 'mips', 'mipsel', 'ppc', 'ppc64', 'riscv64', 's390', 's390x', 'x64', 'unknown'];
18
+ const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
19
+ const semver = /^(?:0|[1-9]\d{0,5})\.(?:0|[1-9]\d{0,5})\.(?:0|[1-9]\d{0,5})(?:-[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)*)?(?:\+[A-Za-z0-9]+(?:[.-][A-Za-z0-9]+)*)?$/;
20
+ const own = (obj, key) => Object.hasOwn(obj, key);
21
+ const fail = (code) => Object.assign(new Error(code), { code });
22
+ function exact(value, keys) {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
24
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
25
+ return false;
26
+ const descriptors = Object.getOwnPropertyDescriptors(value);
27
+ return Object.keys(descriptors).length === keys.length && keys.every(key => own(descriptors, key) && own(descriptors[key], 'value') && descriptors[key].enumerable) &&
28
+ Object.getOwnPropertySymbols(value).length === 0;
29
+ }
30
+ function version(value) { return typeof value === 'string' && value.length <= 64 && semver.test(value) && !/sk-/i.test(value); }
31
+ const isStatus = (value) => typeof value === 'string' && statuses.includes(value);
32
+ const isCheckId = (value) => typeof value === 'string' && own(checkCodes, value);
33
+ function validateDiagnostic(value) {
34
+ try {
35
+ if (!exact(value, ['schema_version', 'report_id', 'generated_at', 'tool', 'environment', 'checks', 'summary', 'privacy', 'network_requests', 'paid_calls']) ||
36
+ value.schema_version !== 1 || typeof value.report_id !== 'string' || !uuid.test(value.report_id) ||
37
+ typeof value.generated_at !== 'string' || value.generated_at.length !== 24 ||
38
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value.generated_at) ||
39
+ !Number.isFinite(Date.parse(value.generated_at)) || new Date(value.generated_at).toISOString() !== value.generated_at ||
40
+ !exact(value.tool, ['name', 'version']) || typeof value.tool.name !== 'string' || !['aptly', 'subagent-decisions'].includes(value.tool.name) || !version(value.tool.version) ||
41
+ !exact(value.environment, ['node', 'platform', 'arch']) || !version(value.environment.node) ||
42
+ typeof value.environment.platform !== 'string' || !platforms.includes(value.environment.platform) ||
43
+ typeof value.environment.arch !== 'string' || !architectures.includes(value.environment.arch) ||
44
+ !Array.isArray(value.checks) || value.checks.length !== Object.keys(checkCodes).length ||
45
+ !exact(value.summary, statuses) || !exact(value.privacy, privacyKeys) || value.network_requests !== 0 || value.paid_calls !== 0)
46
+ throw fail('report_invalid_diagnostic');
47
+ const privacy = value.privacy, summary = value.summary;
48
+ if (privacyKeys.some(key => privacy[key] !== false))
49
+ throw fail('report_invalid_diagnostic');
50
+ const seen = new Set(), counts = { ok: 0, warning: 0, error: 0, unknown: 0 };
51
+ const checks = value.checks.map((check) => {
52
+ if (!exact(check, ['id', 'status', 'code']) || !isCheckId(check.id) || seen.has(check.id) ||
53
+ !isStatus(check.status) || typeof check.code !== 'string' || !checkCodes[check.id].includes(check.code))
54
+ throw fail('report_invalid_diagnostic');
55
+ seen.add(check.id);
56
+ counts[check.status]++;
57
+ return { id: check.id, status: check.status, code: check.code };
58
+ });
59
+ if (statuses.some(key => summary[key] !== counts[key]))
60
+ throw fail('report_invalid_diagnostic');
61
+ return { schema_version: 1, report_id: value.report_id.toLowerCase(), generated_at: value.generated_at,
62
+ tool: { name: value.tool.name, version: value.tool.version },
63
+ environment: { node: value.environment.node, platform: value.environment.platform, arch: value.environment.arch },
64
+ checks, summary: counts, privacy: { task_text: false, paths: false, environment_values: false, credentials: false, raw_errors: false }, network_requests: 0, paid_calls: 0 };
65
+ }
66
+ catch {
67
+ throw fail('report_invalid_diagnostic');
68
+ }
69
+ }
70
+ export function renderDiagnosticMarkdown(diagnostic) {
71
+ const value = validateDiagnostic(diagnostic);
72
+ return [
73
+ '# Diagnostic report', '',
74
+ `Report ID: \`${value.report_id}\``, `Schema version: ${value.schema_version}`, `Generated at: ${value.generated_at}`, '',
75
+ '| Environment | Value |', '| --- | --- |',
76
+ `| Tool | ${value.tool.name} |`, `| Tool version | ${value.tool.version} |`,
77
+ `| Node.js | ${value.environment.node} |`, `| Platform | ${value.environment.platform} |`, `| Architecture | ${value.environment.arch} |`, '',
78
+ '| Check | Status | Code |', '| --- | --- | --- |',
79
+ ...value.checks.map(check => `| ${check.id} | ${check.status} | ${check.code} |`), '',
80
+ `Summary: ok=${value.summary.ok}, warning=${value.summary.warning}, error=${value.summary.error}, unknown=${value.summary.unknown}.`,
81
+ 'Network requests: 0. Paid calls: 0.', '',
82
+ 'This attachment contains only allowlisted diagnostic fields. It contains no task text, private paths, environment values, credentials, or raw errors.',
83
+ 'Key presence does not prove authentication. Host CLI versions were not probed. Review this file before attaching it to an issue; nothing is uploaded automatically.', '',
84
+ ].join('\n');
85
+ }
86
+ function privateDirectory(path, created) {
87
+ const missing = [];
88
+ let existing = path;
89
+ while (true) {
90
+ try {
91
+ lstatSync(existing);
92
+ break;
93
+ }
94
+ catch (error) {
95
+ if (errorCode(error) !== 'ENOENT')
96
+ throw error;
97
+ missing.push(existing);
98
+ const parent = dirname(existing);
99
+ if (parent === existing)
100
+ throw fail('report_output_unavailable');
101
+ existing = parent;
102
+ }
103
+ }
104
+ assertNoRedirectedAncestors(existing);
105
+ for (const dir of missing.reverse()) {
106
+ assertNoRedirectedAncestors(dirname(dir));
107
+ try {
108
+ mkdirSync(dir, { mode: 0o700 });
109
+ created.push(dir);
110
+ chmodSync(dir, 0o700);
111
+ }
112
+ catch (error) {
113
+ if (errorCode(error) !== 'EEXIST')
114
+ throw error;
115
+ }
116
+ assertNoRedirectedAncestors(dir);
117
+ if (!lstatSync(dir).isDirectory())
118
+ throw fail('report_output_unavailable');
119
+ }
120
+ assertNoRedirectedAncestors(path);
121
+ const stat = lstatSync(path);
122
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) ||
123
+ (process.getuid && stat.uid !== process.getuid()))
124
+ throw fail('report_output_unavailable');
125
+ }
126
+ /** Write reviewed, local attachments only. No network, process execution, or config changes. */
127
+ export function exportDiagnosticReport({ diagnostic, format = 'both', outputDir = 'reports' } = {}) {
128
+ const value = validateDiagnostic(diagnostic);
129
+ if (typeof format !== 'string' || !['md', 'json', 'both'].includes(format))
130
+ throw fail('report_invalid_format');
131
+ if (typeof outputDir !== 'string' || !outputDir || outputDir.length > 4096 || outputDir.includes('\0'))
132
+ throw fail('report_output_unavailable');
133
+ const directory = resolve(outputDir), createdDirectories = [], createdFiles = [];
134
+ const extensions = format === 'both' ? ['json', 'md'] : [format];
135
+ const files = extensions.map(extension => `diagnostic-${value.report_id}.${extension}`);
136
+ try {
137
+ privateDirectory(directory, createdDirectories);
138
+ for (let i = 0; i < files.length; i++) {
139
+ // Recheck parents before each exclusive creation; never follow a redirect
140
+ // or replace a pre-existing attachment, even a symlink or directory.
141
+ assertNoRedirectedAncestors(directory);
142
+ const path = join(directory, files[i]);
143
+ let fd;
144
+ try {
145
+ fd = openSync(path, 'wx', 0o600);
146
+ const stat = fstatSync(fd);
147
+ createdFiles.push({ path, dev: stat.dev, ino: stat.ino });
148
+ fchmodSync(fd, 0o600);
149
+ writeFileSync(fd, extensions[i] === 'json' ? JSON.stringify(value, null, 2) + '\n' : renderDiagnosticMarkdown(value));
150
+ }
151
+ finally {
152
+ if (fd !== undefined)
153
+ closeSync(fd);
154
+ }
155
+ }
156
+ return { report_id: value.report_id, files, summary: { ...value.summary } };
157
+ }
158
+ catch (error) {
159
+ // Roll back only attachments created in this operation, preserving an
160
+ // existing collision and any concurrent replacement with a different inode.
161
+ for (const file of createdFiles.reverse()) {
162
+ try {
163
+ const current = lstatSync(file.path);
164
+ if (!current.isSymbolicLink() && current.dev === file.dev && current.ino === file.ino)
165
+ unlinkSync(file.path);
166
+ }
167
+ catch { }
168
+ }
169
+ for (const dir of createdDirectories.reverse()) {
170
+ try {
171
+ rmdirSync(dir);
172
+ }
173
+ catch { }
174
+ }
175
+ throw fail(errorCode(error) === 'EEXIST' ? 'report_already_exists' : 'report_output_unavailable');
176
+ }
177
+ }
@@ -0,0 +1,8 @@
1
+ import { Router } from './core.ts';
2
+ export declare function runtime(env?: NodeJS.ProcessEnv): {
3
+ router: Router;
4
+ mode: "decisions" | "fixture";
5
+ modelInfo: import("./model-info.ts").LoadedModelInfo;
6
+ disabledClients: import("./types.ts").Client[];
7
+ configuration: import("./config.ts").Configuration;
8
+ };
@@ -0,0 +1,22 @@
1
+ import { Router, defaultPolicy } from './core.js';
2
+ import { decisionsProvider, fixtureProvider } from './provider.js';
3
+ import { loadConfiguration } from './config.js';
4
+ import { resolveCatalog } from './catalog.js';
5
+ import { loadModelInfo } from './model-info.js';
6
+ import { assertPrivateStatePath } from './files.js';
7
+ export function runtime(env = process.env) {
8
+ const value = env.ROUTER_PROVIDER ?? 'fixture';
9
+ if (value !== 'fixture' && value !== 'decisions')
10
+ throw new Error('invalid_provider_mode');
11
+ const mode = value;
12
+ if (mode === 'decisions') {
13
+ if (!env.ROUTER_LEDGER_PATH)
14
+ throw new Error('paid_mode_requires_ledger');
15
+ assertPrivateStatePath(env.ROUTER_LEDGER_PATH);
16
+ }
17
+ const modelInfo = loadModelInfo(env.APTLY_MODEL_INFO_PATH);
18
+ const catalog = resolveCatalog(env, modelInfo.facts);
19
+ const policy = { ...defaultPolicy, provider_id: mode, profiles: catalog.profiles, profile_descriptions: catalog.descriptions, catalog_version: catalog.version };
20
+ const router = new Router({ policy, provider: mode === 'fixture' ? fixtureProvider : decisionsProvider({ key: env.OPENAI_API_KEY, descriptions: catalog.descriptions }), ledgerPath: env.ROUTER_LEDGER_PATH });
21
+ return { router, mode, modelInfo, disabledClients: catalog.disabled_clients, configuration: loadConfiguration(env) };
22
+ }
@@ -0,0 +1,7 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer({ env }?: {
3
+ env?: NodeJS.ProcessEnv;
4
+ }): McpServer;
5
+ export declare function serve(options?: {
6
+ env?: NodeJS.ProcessEnv;
7
+ }): Promise<McpServer>;
@@ -0,0 +1,80 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { runtime } from './runtime.js';
6
+ import { VERSION } from './version.js';
7
+ import { hookInput, hookOutput } from './hook.js';
8
+ import { resolveProvenance } from './provenance.js';
9
+ import { loadConfiguration, loadHostContext } from './config.js';
10
+ import { automaticHook, loadAutomaticHookPolicy } from './automatic-hook.js';
11
+ const taskSchema = z.object({ summary: z.string().min(1).max(4000), operation: z.enum(['read_only', 'write', 'review', 'analysis']), context_sufficient: z.boolean() }).strict();
12
+ const constraintsSchema = z.object({ model_pinned: z.boolean().optional(), effort_pinned: z.boolean().optional(), role_pinned: z.boolean().optional(), fork_inherits: z.boolean().optional(), provenance_verified: z.boolean().optional() }).strict();
13
+ const hookSchema = { client: z.enum(['codex', 'claude']), event: z.record(z.string(), z.unknown()) };
14
+ const content = (result) => ({ content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result });
15
+ export function createServer({ env = process.env } = {}) {
16
+ const { router, mode, modelInfo, disabledClients } = runtime(env);
17
+ let lastAutomaticHook = null;
18
+ const server = new McpServer({ name: 'aptly', version: VERSION });
19
+ const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: mode === 'decisions' };
20
+ const evidence = (client, event) => {
21
+ const config = loadConfiguration(env);
22
+ const loaded = loadHostContext(config.context_path);
23
+ const precedence = resolveProvenance({ client, event, context: loaded.context });
24
+ return { config, loaded, precedence };
25
+ };
26
+ server.registerTool('get_routing_capabilities', { description: 'Advisory profile catalog, provider and local budget limits. No paid call.', annotations, inputSchema: {} }, async () => content({
27
+ ...router.capabilities(), provider: mode, hook_mode: loadConfiguration(env).mode,
28
+ precedence: 'per_call_effective_host_evidence_required', automatic_settings_discovery: false,
29
+ recommendation: { application: 'advisory_only', host_evidence_required: false, constraints: 'caller_declared_pins' },
30
+ model_information: { status: modelInfo.status, refresh: 'explicit_only', account_access_verified: false }, disabled_clients: disabledClients,
31
+ automatic_hook: { status: loadAutomaticHookPolicy(env.APTLY_HOOK_POLICY_PATH).reason, mode: loadAutomaticHookPolicy(env.APTLY_HOOK_POLICY_PATH).policy?.mode ?? 'disabled', host_settings_verified: false, last_result: lastAutomaticHook },
32
+ }));
33
+ server.registerTool('recommend_agent', {
34
+ description: 'Recommend an allowlisted profile for a supplied task only; never inspects host settings, spawns, or authorizes an agent. Optional constraints describe caller-declared choices to preserve; omitted flags are not claims about current host settings. Decisions mode may incur a bounded classification request.', annotations,
35
+ inputSchema: { request_id: z.string().min(1).max(96), client: z.enum(['codex', 'claude']), task: taskSchema, eligible_profile_ids: z.array(z.string()).min(1).max(20).optional(), constraints: constraintsSchema.optional() }
36
+ }, async (input) => content({ ...await router.advise(input), provider: mode }));
37
+ server.registerTool('get_routing_precedence', {
38
+ description: 'Explain preserved choices and unknown effective setting sources. Never classifies, reads a transcript, or changes permissions.', annotations: { ...annotations, openWorldHint: false }, inputSchema: hookSchema
39
+ }, async ({ client, event }) => {
40
+ const { precedence, loaded, config } = evidence(client, event);
41
+ return content({ ...precedence, evidence_status: loaded.reason, configuration_status: config.error ?? 'ok', application: 'advisory_only' });
42
+ });
43
+ server.registerTool('route_spawn_hook', {
44
+ description: 'Native PreToolUse Hook. An installed operator policy can select unpinned fresh-agent defaults through Decisions; otherwise the legacy evidence-gated path remains shadow by default. Explicit choices, role/fork/resume and failures preserve the call. Optional task never supplies policy.', annotations,
45
+ inputSchema: { ...hookSchema, task: taskSchema.optional() }
46
+ }, async ({ client, event, task }, extra) => {
47
+ if (env.APTLY_HOOK_POLICY_PATH !== undefined) {
48
+ const result = await automaticHook({ client, event, router, policyPath: env.APTLY_HOOK_POLICY_PATH, signal: extra.signal });
49
+ lastAutomaticHook = result.observation;
50
+ return { content: [{ type: 'text', text: JSON.stringify(result.output) }] };
51
+ }
52
+ const input = hookInput({ client, event, policy: router.policy });
53
+ if (!input)
54
+ return { content: [{ type: 'text', text: '{}' }] };
55
+ const before = evidence(client, event);
56
+ input.constraints = before.precedence.constraints;
57
+ if (task)
58
+ input.task = task;
59
+ const recommendation = await router.recommend(input);
60
+ // Revalidate binding, expiry, source fingerprints and operator configuration
61
+ // after an asynchronous provider call. A changed source cannot enable apply.
62
+ const after = evidence(client, event);
63
+ const apply = before.config.mode === 'apply' && !before.config.error && !after.config.error && after.config.mode === 'apply' &&
64
+ before.config.context_path === after.config.context_path && before.precedence.status === 'eligible' && after.precedence.status === 'eligible' &&
65
+ before.loaded.fingerprint === after.loaded.fingerprint && JSON.stringify(before.loaded.context) === JSON.stringify(after.loaded.context);
66
+ const output = hookOutput({ client, event, recommendation, mode: apply ? 'apply' : 'shadow', compatibilityVerified: apply, policy: router.policy });
67
+ return { content: [{ type: 'text', text: JSON.stringify(output) }] };
68
+ });
69
+ return server;
70
+ }
71
+ export async function serve(options) { const server = createServer(options); await server.connect(new StdioServerTransport()); return server; }
72
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
73
+ try {
74
+ await serve();
75
+ }
76
+ catch {
77
+ process.stderr.write('aptly: server_configuration_error\n');
78
+ process.exitCode = 1;
79
+ }
80
+ }
@@ -0,0 +1,22 @@
1
+ type Client = 'codex' | 'claude';
2
+ export interface SetupOptions {
3
+ home?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ packageRoot?: string;
6
+ automatic?: boolean;
7
+ clients?: Client[];
8
+ mode?: 'defaults' | 'advisory';
9
+ }
10
+ export interface SetupClientResult {
11
+ client: Client;
12
+ status: 'configured' | 'unchanged' | 'removed' | 'skipped' | 'error';
13
+ codes: string[];
14
+ requires_hook_review?: boolean;
15
+ }
16
+ export interface SetupResult {
17
+ status: 'complete' | 'partial' | 'skipped';
18
+ clients: SetupClientResult[];
19
+ }
20
+ export declare function setupHosts(options?: SetupOptions): SetupResult;
21
+ export declare function removeSetup(options?: SetupOptions): SetupResult;
22
+ export {};