@corelayer-ai/cli 0.5.2 → 0.5.3

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 CHANGED
@@ -1,17 +1,3 @@
1
- # CLI credential security
1
+ # Corelayer CLI
2
2
 
3
- Browser login requires a user session and issues a key valid for 24 hours. API keys cannot authorize another CLI login. Sign in again when the key expires.
4
-
5
- User-owned API keys require a current organization membership on every authentication. Their effective role cannot exceed either the key's stored role or the owner's current role. Removing a user blocks their keys once the membership change reaches Corelayer; demoting an admin immediately limits their keys after synchronization. This also applies to user-owned automation keys. Keys with no user owner retain their existing service-key behavior and require explicit revocation/expiry management.
6
-
7
- `corelayer logout` revokes the credential saved in `~/.corelayer/config.json` at its saved API URL before clearing the file. A failed revocation leaves the credential available for retry. `corelayer logout --local-only` explicitly removes the local copy without revoking the remote credential, for offline cleanup or an already expired/revoked key. A key in `CORELAYER_API_KEY` is not revoked by logout; unset that variable to stop using it and revoke it through key management when needed. Revoking a shared saved API key also affects other consumers of that same key.
8
-
9
- `corelayer config get` and its JSON output omit the token. Preflight invokes Git without a shell, resolves refs to commit IDs, and disables external diff/text-conversion helpers.
10
-
11
- ## Deploying the fix
12
-
13
- Deploy the platform database service and API server changes before distributing the updated CLI: logout requires the new `cli-auth.logout` operation and `/api/v1/cli/logout` endpoint. No schema migration is required.
14
-
15
- Existing CLI keys do not retroactively acquire the new 24-hour expiry. Revoke those old keys through key management and have users log in again. Live membership/role enforcement applies to existing user-owned keys once the server change is deployed. For incident response, explicitly revoke affected keys rather than relying only on local logout or identity-provider synchronization.
16
-
17
- These changes address the audited defects. They do not introduce OAuth PKCE, granular read-only/resource scopes, OS-keychain storage, npm provenance, or enterprise SSO-policy configuration; those remain separate enterprise-readiness work.
3
+ Source and packaging for the `@corelayer-ai/cli` command-line client. Provides authentication, production issue investigation and actions, organization memory queries, schedules, preflight checks, and MCP integration. The `skill/` folder contains the agent instructions distributed with the CLI.
@@ -0,0 +1,163 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFlag } from "../lib/args.js";
3
+ import { fail, printJson } from "../lib/output.js";
4
+ function ticketInput(args, config) {
5
+ const provider = readFlag(args, "--provider");
6
+ if (provider !== "jira" && provider !== "linear")
7
+ fail("--provider jira|linear is required.");
8
+ const groupId = readFlag(args, "--group") || config.defaults?.group;
9
+ if (!groupId)
10
+ fail("--group is required (or configure default-group).");
11
+ const input = { groupId, provider };
12
+ for (const [flag, key] of [
13
+ ["--account", "accountId"],
14
+ ["--project", "projectId"],
15
+ ["--team", "teamId"],
16
+ ["--type", "issueTypeId"],
17
+ ["--query", "query"],
18
+ ["--ticket", "ticketId"],
19
+ ]) {
20
+ const value = readFlag(args, flag);
21
+ if (value)
22
+ input[key] = value;
23
+ }
24
+ const revision = readFlag(args, "--revision");
25
+ if (revision) {
26
+ const value = Number(revision);
27
+ if (!Number.isSafeInteger(value) || value < 1)
28
+ fail("--revision must be a positive integer from ticket destinations.");
29
+ input.expectedRevision = value;
30
+ }
31
+ const fields = readFlag(args, "--fields");
32
+ if (fields) {
33
+ try {
34
+ const parsed = JSON.parse(fields);
35
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
36
+ fail("--fields must be a JSON object.");
37
+ input.fields = parsed;
38
+ }
39
+ catch {
40
+ fail("--fields must be a JSON object.");
41
+ }
42
+ }
43
+ return input;
44
+ }
45
+ function ticketIdentity(args, mutating) {
46
+ const suppliedId = readFlag(args, "--request-id");
47
+ const requestId = suppliedId || (mutating ? randomUUID() : undefined);
48
+ if ((mutating || suppliedId !== undefined) &&
49
+ (!requestId ||
50
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(requestId)))
51
+ fail("A valid --request-id UUID is required.");
52
+ return requestId;
53
+ }
54
+ function mutationInput(action, input, requestId) {
55
+ if (!input.accountId)
56
+ fail("--account is required; use ticket destinations to discover accounts.");
57
+ if (action === "link" && !input.ticketId)
58
+ fail("--ticket is required for link.");
59
+ input.requestId = requestId;
60
+ input.intent =
61
+ action === "create"
62
+ ? "create_new"
63
+ : action === "link"
64
+ ? "link_existing"
65
+ : "find_or_create";
66
+ process.stderr.write(`Ticket request: ${requestId}\n`);
67
+ }
68
+ function interruptedRequest(error) {
69
+ return (error instanceof Error &&
70
+ (/timed out|fetch failed|socket|network/i.test(error.message) ||
71
+ ("status" in error &&
72
+ typeof error.status === "number" &&
73
+ error.status >= 500)));
74
+ }
75
+ function resultExitCode(status) {
76
+ const codes = {
77
+ needs_input: 2,
78
+ pending: 3,
79
+ in_progress: 3,
80
+ recovery_required: 4,
81
+ blocked: 4,
82
+ stale_request: 2,
83
+ failed: 1,
84
+ error: 1,
85
+ };
86
+ return typeof status === "string" ? codes[status] || 0 : 0;
87
+ }
88
+ function shellArgument(value) {
89
+ return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
90
+ }
91
+ function resumeFlags(input, requestId) {
92
+ const flags = [
93
+ ["groupId", "--group"],
94
+ ["provider", "--provider"],
95
+ ["accountId", "--account"],
96
+ ["projectId", "--project"],
97
+ ["teamId", "--team"],
98
+ ["issueTypeId", "--type"],
99
+ ["expectedRevision", "--revision"],
100
+ ];
101
+ const values = flags
102
+ .filter(([key]) => input[key] !== undefined)
103
+ .map(([key, flag]) => `${flag} ${shellArgument(input[key])}`);
104
+ if (input.fields)
105
+ values.push(`--fields ${shellArgument(JSON.stringify(input.fields))}`);
106
+ values.push(`--request-id ${shellArgument(requestId)}`);
107
+ return values.join(" ");
108
+ }
109
+ function humanTicketResult(result, issueId, input, requestId) {
110
+ const lines = [
111
+ String(result.message || result.status || JSON.stringify(result, null, 2)),
112
+ ];
113
+ const candidates = Array.isArray(result.candidates)
114
+ ? result.candidates.filter((candidate) => candidate !== null && typeof candidate === "object")
115
+ : [];
116
+ for (const [index, candidate] of candidates.entries()) {
117
+ lines.push(`${index + 1}. ${candidate.identifier || candidate.id}: ${candidate.title || ""}${candidate.evidence ? ` — ${candidate.evidence}` : ""}`);
118
+ if (candidate.url)
119
+ lines.push(` ${candidate.url}`);
120
+ }
121
+ const resumeId = typeof result.requestId === "string" ? result.requestId : requestId;
122
+ if (result.status === "needs_input" && resumeId) {
123
+ const flags = resumeFlags(input, resumeId);
124
+ const firstTicket = candidates[0]?.id;
125
+ if (firstTicket)
126
+ lines.push(`Link an existing ticket: corelayer issues ticket link ${shellArgument(issueId)} ${flags} --ticket ${shellArgument(firstTicket)}`);
127
+ lines.push(`Create a new ticket: corelayer issues ticket create ${shellArgument(issueId)} ${flags}`);
128
+ if (result.requirements)
129
+ lines.push(`Required ticket details: ${JSON.stringify(result.requirements, null, 2)}`);
130
+ }
131
+ return lines.join("\n");
132
+ }
133
+ export async function runIssueTickets(args, client, config, ctx) {
134
+ const [action, issueId] = args;
135
+ if (!issueId ||
136
+ !["destinations", "search", "ensure", "create", "link", "status"].includes(action))
137
+ fail("Usage: corelayer issues ticket <destinations|search|ensure|create|link|status> <id|slug> --provider jira|linear --group <id>");
138
+ const input = ticketInput(args, config);
139
+ const mutating = ["ensure", "create", "link"].includes(action);
140
+ const requestId = ticketIdentity(args, mutating);
141
+ if (mutating)
142
+ mutationInput(action, input, requestId);
143
+ const suffixes = {
144
+ destinations: "/destinations",
145
+ search: "/search",
146
+ status: `/requests/${encodeURIComponent(requestId || "current")}`,
147
+ };
148
+ let result;
149
+ try {
150
+ result = await client.ticketRequest(mutating || action === "search" ? "POST" : "GET", issueId, suffixes[action] || "", input);
151
+ }
152
+ catch (error) {
153
+ if (!mutating || !interruptedRequest(error))
154
+ throw error;
155
+ process.stderr.write("Response interrupted; checking the same ticket request.\n");
156
+ result = await client.ticketRequest("GET", issueId, `/requests/${encodeURIComponent(requestId)}`, { groupId: input.groupId, provider: input.provider });
157
+ }
158
+ if (ctx.json)
159
+ printJson(result);
160
+ else
161
+ process.stdout.write(`${humanTicketResult(result, issueId, input, requestId)}\n`);
162
+ process.exitCode = resultExitCode(result.status);
163
+ }
@@ -4,6 +4,7 @@ import { CorelayerClient } from "../lib/api-client.js";
4
4
  import { readFlag } from "../lib/args.js";
5
5
  import { readConfig, resolveApiUrl, resolveToken } from "../lib/config.js";
6
6
  import { fail, formatDate, printJson, printTable } from "../lib/output.js";
7
+ import { runIssueTickets } from "./issue-tickets.js";
7
8
  function hasFlag(args, flag) {
8
9
  return args.includes(flag);
9
10
  }
@@ -53,6 +54,7 @@ USAGE:
53
54
  SUBCOMMANDS:
54
55
  list List issues in a group
55
56
  get <id|slug> Show details for one issue
57
+ ticket <action> <id> Jira/Linear destinations, search, ensure, create, link, status
56
58
  summary Summarize issue health for a group
57
59
  diff Show issues new or changed since a timestamp
58
60
  close <id|slug> Close an issue (write operation)
@@ -75,6 +77,15 @@ OPTIONS:
75
77
  Diff: ISO 8601 timestamp or preset (1hour, 24hours, 7days, 14days, 30days)
76
78
  --duration <duration>
77
79
  Diff: human-friendly duration (e.g. 2hours, 4hours, 30m, 1d)
80
+ --provider jira|linear Ticket provider
81
+ --account <id> Connected ticket account
82
+ --project <id> Ticket project
83
+ --team <id> Linear team
84
+ --type <id> Jira issue type
85
+ --ticket <id> Selected existing ticket (link)
86
+ --request-id <uuid> Resume the same mutation or read its status
87
+ --revision <n> Jira account revision from ticket destinations
88
+ --fields <json> Required provider creation fields
78
89
  --yes Skip confirmation prompt (delete)
79
90
  --json Output as JSON
80
91
  -h, --help Print help
@@ -299,6 +310,10 @@ export async function runIssues(args, ctx) {
299
310
  }
300
311
  const apiUrl = resolveApiUrl(ctx.apiUrlOverride);
301
312
  const client = new CorelayerClient(apiUrl, token);
313
+ if (sub === "ticket") {
314
+ await runIssueTickets(args.slice(1), client, config, ctx);
315
+ return;
316
+ }
302
317
  const commandArgs = removeFlags(args.slice(1));
303
318
  const handler = ISSUE_COMMAND_HANDLERS[sub];
304
319
  if (!handler) {
@@ -129,6 +129,10 @@ function buildPreflightContext(args) {
129
129
  return { base, description, repo, title };
130
130
  }
131
131
  function printPreflightResult(result, ctx) {
132
+ if (result.degraded) {
133
+ process.stderr.write(`⚠ Preflight could not run (${result.reason ?? "unknown reason"}). This is not an all-clear.\n`);
134
+ return;
135
+ }
132
136
  if (result.checks.length === 0) {
133
137
  if (!ctx.quiet) {
134
138
  process.stdout.write("✓ Preflight: nothing in org memory to double-check for this change.\n");
@@ -25,9 +25,9 @@ export class CorelayerClient {
25
25
  this.token = token;
26
26
  this.anomalies = new AnomalyApiClient((method, path) => this.request(method, path));
27
27
  }
28
- async request(method, path, body) {
28
+ async request(method, path, body, timeoutMs = 30_000) {
29
29
  const controller = new AbortController();
30
- const timer = setTimeout(() => controller.abort(), 30_000);
30
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
31
31
  let res;
32
32
  try {
33
33
  res = await fetch(`${this.baseUrl}${path}`, {
@@ -51,10 +51,17 @@ export class CorelayerClient {
51
51
  clearTimeout(timer);
52
52
  }
53
53
  if (!res.ok) {
54
- throw new Error(await parseError(res));
54
+ throw Object.assign(new Error(await parseError(res)), {
55
+ status: res.status,
56
+ });
55
57
  }
56
58
  return (await res.json());
57
59
  }
60
+ async ticketRequest(method, issueId, action, input) {
61
+ const base = `/api/v1/issues/${encodeURIComponent(issueId)}/tickets${action}`;
62
+ const query = new URLSearchParams(Object.entries(input).map(([key, value]) => [key, String(value)]));
63
+ return this.request(method, method === "GET" ? `${base}?${query}` : base, method === "POST" ? input : undefined, 120_000);
64
+ }
58
65
  async listIssues(groupId, filters) {
59
66
  const params = new URLSearchParams({ groupId });
60
67
  for (const [key, value] of Object.entries(filters)) {
@@ -196,7 +203,9 @@ export async function exchangeCliCode(baseUrl, code) {
196
203
  clearTimeout(timer);
197
204
  }
198
205
  if (!res.ok) {
199
- throw new Error(await parseError(res));
206
+ throw Object.assign(new Error(await parseError(res)), {
207
+ status: res.status,
208
+ });
200
209
  }
201
210
  return (await res.json());
202
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corelayer-ai/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Corelayer CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "@corelayer-ai/sdk": "^0.2.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/node": "^20.17.57",
25
+ "@types/node": "^24.19.2",
26
26
  "tsx": "^4.22.4",
27
27
  "typescript": "^5.8.3"
28
28
  }
package/skill/SKILL.md CHANGED
@@ -22,6 +22,7 @@ Corelayer is an AI-native production support platform. Use it to inspect live is
22
22
  - **[Production health](#production-health)** — Summarize and list current issues
23
23
  - **[Issue investigation](#issue-investigation)** — Read evidence, root cause, and next steps
24
24
  - **[Post-deploy triage](#post-deploy-triage)** — Find issues new or changed after a release
25
+ - **[ECS monitoring recovery](references/ecs-monitor-recovery.md)** — Preview and explicitly acknowledge skipped coverage in a validated AWS tenant runtime
25
26
  - **[Preflight](#preflight-before-a-pull-request)** — Check a change against the organization's memory
26
27
  - **[Issue actions](#issue-actions)** — Close, reopen, bulk-close, or delete with approval
27
28
  - **[MCP server](#mcp-server)** — Give agents Corelayer tools directly
@@ -303,7 +304,6 @@ Remote MCP is read-only. It exposes:
303
304
  - `corelayer.get_issue`
304
305
  - `corelayer.get_issue_summary`
305
306
  - `corelayer.list_integrations`
306
- - `corelayer.search_org_memory`
307
307
  - `corelayer.preflight`
308
308
 
309
309
  Call `corelayer.list_groups` first and pass the selected `id` as `groupId` to group-scoped tools.
@@ -0,0 +1,67 @@
1
+ # ECS monitoring cursor recovery
2
+
3
+ Use this procedure only when an operator explicitly asks to recover an expired ECS monitoring cursor.
4
+
5
+ Supported scope: an enabled, validated tenant runtime with both deployment and organization binding, AWS data-plane mode, and the tenant monitoring table. The legacy `infra/environments/customer-template` ECS deployment has no such binding and is deliberately unsupported by this command. Do not enable global tenant mode, copy credentials, or broaden its `__system__` policy to bypass that boundary. A caller rejected as unbound needs the supported tenant deployment contract before this procedure applies.
6
+
7
+ ECS stopped-task history is finite. Normal polling refuses a cursor outside its
8
+ supported history window rather than silently skipping evidence. Fix the original
9
+ polling failure before recovery. CloudWatch log-only cursors can catch up normally;
10
+ this tool is only for an active monitored AWS ECS service in an enabled customer
11
+ runtime using AWS DynamoDB.
12
+
13
+ The release includes `dist/bin/recover-ecs-monitor-cursor.js` in the engine's
14
+ `controls` container. An authorized operator runs it through the installation's
15
+ normal private Kubernetes access. It uses the runtime's organization, deployment,
16
+ region, table and AWS identity; it accepts no scope or endpoint overrides.
17
+
18
+ Preview without writing:
19
+
20
+ ```sh
21
+ kubectl -n corelayer exec deploy/engine -c controls -- \
22
+ node dist/bin/recover-ecs-monitor-cursor.js --resource RESOURCE_UUID \
23
+ --reason 'Resume after fixing the monitoring outage'
24
+ ```
25
+
26
+ Review the returned `scope`, `intent` and `skippedInterval`. The interval
27
+ `[expectedCursor, resumeAt)` has **unverified coverage**. Acknowledging it does not
28
+ recover historical events or mark them processed. Review any independent retained
29
+ logs and incident evidence separately. The preview is not a successful recovery.
30
+
31
+ Apply the exact reviewed intent, replacing every placeholder with the preview's
32
+ value. Approval expires after ten minutes; the resume point must still be within
33
+ the last fifty minutes and must not be in the future.
34
+
35
+ ```sh
36
+ kubectl -n corelayer exec deploy/engine -c controls -- \
37
+ node dist/bin/recover-ecs-monitor-cursor.js --resource RESOURCE_UUID \
38
+ --expected-cursor EXPECTED_MS --resume-at RESUME_MS \
39
+ --request-id REQUEST_UUID --approved-at APPROVED_MS \
40
+ --reason 'Resume after fixing the monitoring outage' --apply --acknowledge-gap
41
+ ```
42
+
43
+ One transaction compares the stored cursor to the reviewed value, confirms that no migration hold exists, advances the cursor and creates an immutable recovery audit record in the same customer table. The audit retains `ApprovedAt`; this recovery write uses that same approved intent time for the cursor's `UpdatedAt`. It does not claim a server commit time. No fresh wall-clock value enters the transaction, so identical retries retain both the token and transaction parameters while the first attempt is pending. Normal
44
+ polling cannot erase that separate record. The operation affects only the selected
45
+ normal ECS cursor; anomaly cursors are unchanged. Concurrent progress or recovery
46
+ causes the conditional write to fail without discarding the newer cursor. Obtain
47
+ a fresh preview if the cursor or selected resource changed.
48
+
49
+ If the response is ambiguous, retry the **same** reviewed inputs and request ID.
50
+ A matching durable audit returns `already_applied`, including after the preview
51
+ expires. A request ID reused with different intent is rejected. Do not delete the
52
+ cursor or use integration disconnect cleanup to bypass this protocol.
53
+
54
+ After `applied` or `already_applied`, verify normal polling and durable alert
55
+ publication, then run a fresh controlled error and follow issue creation, analysis
56
+ completion, and its rendered customer UI. Recovery alone is not analysis success.
57
+
58
+ ## Local transaction verification
59
+
60
+ `scripts/ecs-cursor-recovery.dynamodb.ts` exercises the production recovery module
61
+ against an isolated DynamoDB Local endpoint. It creates and removes one test table
62
+ and rejects non-loopback targets:
63
+
64
+ ```sh
65
+ DYNAMODB_LOCAL_ENDPOINT=http://127.0.0.1:58039 \
66
+ node --import tsx scripts/ecs-cursor-recovery.dynamodb.ts
67
+ ```
@@ -57,6 +57,8 @@ Each check is one reminder plus its grounding:
57
57
  (it never blocks a push). Surface them to the user and let them decide.
58
58
  - An **empty result is normal and useful** — it means nothing in memory maps to this change.
59
59
  Don't manufacture concerns; say memory had nothing to flag.
60
+ - A result with `degraded: true` means the check **did not run** (its `reason` says why).
61
+ Tell the user preflight could not run; never report it as an all-clear.
60
62
  - Higher `seen N×` and `critical`/`high` severity deserve the most attention.
61
63
 
62
64
  ## MCP equivalent