diff --git a/ERRORS.md b/ERRORS.md index 8fd9f49..2d0c809 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -123,7 +123,7 @@ Hint: polylane workspace use These are the non-obvious command-level behaviours an agent should know about. Specific error strings are subject to change — use these as categories you can rely on. -### Destructive operations (`auth logout`, `integration disconnect`, `cloud disconnect`, `memory delete`, …) +### Destructive operations (`auth logout`, `integration disconnect`, `cloud disconnect`, `artifact delete`, …) - Prompt for confirmation in interactive mode. - Require `--yes` in non-interactive mode, else exit `2`. diff --git a/README.md b/README.md index 20cbb61..3ad4f52 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,6 @@ polylane issue note "rolled back deploy" polylane service logs --since 1h --grep error polylane tools run findNodes --params '{"query":"api"}' polylane thread ask "" -polylane memory save "" ``` ## Discovering commands diff --git a/skill/SKILL.md b/skill/SKILL.md index 6e8b2c9..93d6aeb 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,6 +1,6 @@ --- name: polylane-cli -description: Use `polylane` to investigate production issues, explore cloud infrastructure (logs / metrics / dependency graphs), find the repositories that deploy a service, save memories, connect observability tools and cloud accounts, and drive threads with the Polylane agent. Use when the user wants to debug a production issue, look up a service, find a repository, manage integrations, connect a cloud provider, or talk to the Polylane agent from the terminal. +description: Use `polylane` to investigate production issues, explore cloud infrastructure (logs / metrics / dependency graphs), find the repositories that deploy a service, connect observability tools and cloud accounts, and drive threads with the Polylane agent. Use when the user wants to debug a production issue, look up a service, find a repository, manage integrations, connect a cloud provider, or talk to the Polylane agent from the terminal. --- # Polylane CLI — Agent Skill Guide @@ -178,17 +178,14 @@ polylane issue timeline # who did what, when polylane issue note "rolled back deploy ABC" polylane issue milestone "Mitigated" -# Drill into services -polylane service find "" +# Drill into services (a service ID is ////) +polylane service list # discover services and their ids polylane service logs --since 1h --grep error polylane service metrics --metric --since 1h polylane service graph --direction both --depth 1 # Find the repository, then search it in a local clone polylane repo find "" - -# Save what you learned -polylane memory save "" ``` ### Running agent tools directly @@ -237,8 +234,8 @@ Attach resources as context by passing their IDs — the CLI infers the resource # Extract a field polylane workspace list --output json | jq '.items[0].id' -# Chain: find a service → check its logs -SVC=$(polylane service find "payment lambda" --output json --quiet | jq -r '.items[0].id') +# Chain: pick a service → check its logs +SVC=$(polylane service list --output json --quiet | jq -r '.items[0].id') polylane service logs "$SVC" --since 1h --grep error # Fire-and-forget + later poll diff --git a/src/commands/feed/index.ts b/src/commands/feed/index.ts deleted file mode 100644 index 8265a56..0000000 --- a/src/commands/feed/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { feedListCommand } from './list'; -import { feedSummaryCommand } from './summary'; - -export const feedCommands = [feedListCommand, feedSummaryCommand]; diff --git a/src/commands/feed/list.ts b/src/commands/feed/list.ts deleted file mode 100644 index 5c46eae..0000000 --- a/src/commands/feed/list.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatList, outputJson } from '../../output/formatter'; -import { projectItems } from '../../output/project'; -import { - requireWorkspace, - getArgString, - getArgNumber, - getArgBoolean, - parseDuration, -} from '../helpers'; -import { CLIError } from '../../errors/base'; -import { ExitCode } from '../../errors/codes'; - -type Category = NonNullable[1]>['category']>; - -// Retired category; the Exclude keeps this compiling until the API drops it too. -const CATEGORY_COVERAGE: Record, true> = { - autofix: true, - change: true, - release: true, - issue: true, - digest: true, - tier: true, -}; - -const VALID_CATEGORIES = Object.keys(CATEGORY_COVERAGE) as Category[]; - -const FIELDS = [ - 'occurredAt', - 'category', - 'action', - 'title', - 'subtitle', - 'actorName', - 'actorType', - 'resourceType', - 'resourceId', - 'childCount', - '_html_url', -]; - -export const feedListCommand: Command = { - name: 'feed list', - description: 'Workspace agent activity feed (autofixes, changes, releases, issues…)', - operationId: 'feed.list', - options: [ - { flag: '--category ', description: VALID_CATEGORIES.join(' | '), type: 'string' }, - { flag: '--since ', description: 'Only entries from the last (e.g. 24h, 7d, 30m)', type: 'string' }, - { flag: '--from ', description: 'Occurred at or after (unix ms)', type: 'number' }, - { flag: '--to ', description: 'Occurred at or before (unix ms)', type: 'number' }, - { flag: '--limit ', description: 'Max items, 1–200 (default 20)', type: 'number' }, - { flag: '--cursor ', description: 'Pagination cursor from a previous nextCursor', type: 'string' }, - { flag: '--full', description: 'Return full entries (metadata + nested children)', type: 'boolean' }, - ], - examples: [ - 'polylane feed list', - 'polylane feed list --since 24h', - 'polylane feed list --category issue --limit 50', - 'polylane feed list --cursor ', - ], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - - const category = getArgString(args, 'category'); - if (category && !VALID_CATEGORIES.includes(category as Category)) { - throw new CLIError( - `Invalid --category: ${category}`, - ExitCode.USAGE, - `Must be one of: ${VALID_CATEGORIES.join(', ')}` - ); - } - - const limit = getArgNumber(args, 'limit') ?? 20; - if (limit < 1 || limit > 200) { - throw new CLIError(`Invalid --limit: ${limit}`, ExitCode.USAGE, 'Use 1–200'); - } - - const since = getArgString(args, 'since'); - const fromMs = getArgNumber(args, 'from') ?? (since ? Date.now() - parseDuration(since) : undefined); - const toMs = getArgNumber(args, 'to'); - const cursor = getArgString(args, 'cursor'); - const full = getArgBoolean(args, 'full') === true; - - const api = new PolylaneAPI(config); - const result = await api.feedList(workspaceId, { - limit, - ...(category ? { category: category as Category } : {}), - ...(fromMs !== undefined ? { from: fromMs } : {}), - ...(toMs !== undefined ? { to: toMs } : {}), - ...(cursor ? { cursor } : {}), - }); - - const entries = result.entries ?? []; - - if (config.output === 'json') { - outputJson({ items: entries, count: entries.length, nextCursor: result.nextCursor }); - return; - } - - if (full) { - formatList(config, { items: entries as unknown as Array>, count: entries.length }); - writeCursorHint(config, result.nextCursor); - return; - } - - const withChildCount = entries.map((e) => ({ - ...(e as unknown as Record), - childCount: e.children?.length ?? 0, - })); - - formatList( - config, - { - items: projectItems(withChildCount, FIELDS), - count: entries.length, - }, - { - headers: ['When', 'Category', 'Action', 'Title', 'Actor'], - rows: (item) => [ - String(item.occurredAt ?? ''), - String(item.category ?? ''), - String(item.action ?? ''), - truncate(withGroup(String(item.title ?? ''), Number(item.childCount ?? 0)), 60), - String(item.actorName ?? item.actorType ?? ''), - ], - } - ); - writeCursorHint(config, result.nextCursor); - }, -}; - -function withGroup(title: string, childCount: number): string { - return childCount > 0 ? `${title} (+${childCount})` : title; -} - -function truncate(s: string, max: number): string { - return s.length > max ? s.slice(0, max - 1) + '…' : s; -} - -function writeCursorHint(config: Config, nextCursor: string | null): void { - if (config.quiet || !nextCursor) return; - process.stdout.write(`\nMore: polylane feed list --cursor ${nextCursor}\n`); -} diff --git a/src/commands/feed/summary.ts b/src/commands/feed/summary.ts deleted file mode 100644 index 80cd662..0000000 --- a/src/commands/feed/summary.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { outputJson, outputTable } from '../../output/formatter'; -import { requireWorkspace, getArgString, getArgNumber, parseDuration } from '../helpers'; - -export const feedSummaryCommand: Command = { - name: 'feed summary', - description: 'Activity aggregate over time (autofix PR counts per day)', - operationId: 'feed.summary', - options: [ - { flag: '--since ', description: 'Window to aggregate over (e.g. 7d, 30d)', type: 'string' }, - { flag: '--from ', description: 'Start of window (unix ms)', type: 'number' }, - { flag: '--to ', description: 'End of window (unix ms)', type: 'number' }, - ], - examples: ['polylane feed summary', 'polylane feed summary --since 30d'], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - - const since = getArgString(args, 'since'); - const fromMs = getArgNumber(args, 'from') ?? (since ? Date.now() - parseDuration(since) : undefined); - const toMs = getArgNumber(args, 'to'); - - const api = new PolylaneAPI(config); - const result = await api.feedSummary(workspaceId, { - ...(fromMs !== undefined ? { from: fromMs } : {}), - ...(toMs !== undefined ? { to: toMs } : {}), - }); - - if (config.output === 'json') { - outputJson(result); - return; - } - - process.stdout.write(`Autofix PRs: ${result.autofixPrs}\n\n`); - const daily = result.autofixDaily ?? []; - if (daily.length === 0) { - process.stdout.write('(no daily activity)\n'); - return; - } - outputTable( - ['Date', 'Autofix PRs'], - daily.map((d) => [String(d.date), String(d.count)]) - ); - }, -}; diff --git a/src/commands/index.ts b/src/commands/index.ts index ffbc79a..8887f8c 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -4,11 +4,9 @@ import { configCommands } from './config'; import { helpCommand } from './help'; import { setupCommand } from './setup'; import { updateCommand } from './update'; -import { feedCommands } from './feed'; import { issueCommands } from './issue'; import { serviceCommands } from './service'; import { repoCommands } from './repo'; -import { memoryCommands } from './memory'; import { threadCommands } from './thread'; import { integrationCommands } from './integration'; import { cloudCommands } from './cloud'; @@ -16,7 +14,6 @@ import { workspaceCommands } from './workspace'; import { subscriptionCommands } from './subscription'; import { apiCommands } from './api'; import { telemetryCommands } from './telemetry'; -import { noteCommands } from './note'; import { autofixCommands } from './autofix'; import { artifactCommands } from './artifact'; import { toolsCommands } from './tools'; @@ -28,17 +25,14 @@ export function registerAllCommands(): void { registered = true; const all = [ - ...feedCommands, ...issueCommands, ...serviceCommands, ...repoCommands, - ...memoryCommands, ...threadCommands, ...integrationCommands, ...cloudCommands, ...workspaceCommands, ...subscriptionCommands, - ...noteCommands, ...autofixCommands, ...artifactCommands, ...toolsCommands, diff --git a/src/commands/memory/delete.ts b/src/commands/memory/delete.ts deleted file mode 100644 index 8e2e005..0000000 --- a/src/commands/memory/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { requireWorkspace, requirePositional, getArgBoolean } from '../helpers'; -import { promptConfirm } from '../../utils/prompt'; -import { isInteractive } from '../../utils/env'; - -export const memoryDeleteCommand: Command = { - name: 'memory delete', - description: 'Delete a memory', - operationId: 'memories.del', - positional: [{ name: 'memory-id', description: 'The memory ID' }], - options: [{ flag: '--yes', description: 'Skip confirmation', type: 'boolean' }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const id = requirePositional(args, 0, 'memory-id'); - - if (getArgBoolean(args, 'yes') !== true && isInteractive(config.nonInteractive)) { - const ok = await promptConfirm( - { nonInteractive: config.nonInteractive }, - `Delete memory ${id}?`, - false - ); - if (!ok) { - process.stderr.write('Cancelled. Nothing changed.\n'); - return; - } - } - - const api = new PolylaneAPI(config); - const result = await api.memoriesDel(workspaceId, id); - process.stdout.write(`Deleted ${result.id}\n`); - }, -}; diff --git a/src/commands/memory/find.ts b/src/commands/memory/find.ts deleted file mode 100644 index 0bdb2c9..0000000 --- a/src/commands/memory/find.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatList } from '../../output/formatter'; -import { projectItems } from '../../output/project'; -import { requireWorkspace, requirePositional, getAllPositional } from '../helpers'; - -const FIELDS = ['id', 'title', 'labels', 'sourceType', 'created']; - -export const memoryFindCommand: Command = { - name: 'memory find', - description: 'Search memories by prompt', - operationId: 'memories.search', - positional: [{ name: 'prompt', description: 'Search query', variadic: true }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - requirePositional(args, 0, 'prompt'); - const prompt = getAllPositional(args).join(' '); - - const api = new PolylaneAPI(config); - const items = await api.memoriesSearch({ workspaceId, prompt }); - const projected = projectItems(items as unknown as Array>, FIELDS); - - formatList( - config, - { items: projected, count: projected.length }, - { - headers: ['ID', 'Title', 'Labels', 'Source', 'Created'], - rows: (item) => [ - String(item.id ?? ''), - String(item.title ?? ''), - Array.isArray(item.labels) ? item.labels.join(',') : '', - String(item.sourceType ?? ''), - String(item.created ?? ''), - ], - } - ); - }, -}; diff --git a/src/commands/memory/index.ts b/src/commands/memory/index.ts deleted file mode 100644 index e218ce2..0000000 --- a/src/commands/memory/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { memoryListCommand } from './list'; -import { memoryFindCommand } from './find'; -import { memoryShowCommand } from './show'; -import { memorySaveCommand } from './save'; -import { memoryDeleteCommand } from './delete'; - -export const memoryCommands = [ - memoryListCommand, - memoryFindCommand, - memoryShowCommand, - memorySaveCommand, - memoryDeleteCommand, -]; diff --git a/src/commands/memory/list.ts b/src/commands/memory/list.ts deleted file mode 100644 index f45b361..0000000 --- a/src/commands/memory/list.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatList } from '../../output/formatter'; -import { projectItems } from '../../output/project'; -import { requireWorkspace, getArgNumber, getArgBoolean } from '../helpers'; - -const FIELDS = ['id', 'title', 'labels', 'sourceType', 'created']; - -export const memoryListCommand: Command = { - name: 'memory list', - description: 'List memories', - operationId: 'memories.list', - options: [ - { flag: '--limit ', description: 'Max items (default 20)', type: 'number' }, - { flag: '--full', description: 'Return full objects', type: 'boolean' }, - ], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const limit = getArgNumber(args, 'limit') ?? 20; - const full = getArgBoolean(args, 'full') === true; - - const api = new PolylaneAPI(config); - const result = await api.memoriesList(workspaceId, { perPage: limit }); - - if (full) { - formatList(config, result as { items: Array>; count: number }); - return; - } - - formatList( - config, - { - items: projectItems(result.items as unknown as Array>, FIELDS), - count: result.count, - }, - { - headers: ['ID', 'Title', 'Labels', 'Source', 'Created'], - rows: (item) => [ - String(item.id ?? ''), - truncate(String(item.title ?? ''), 50), - Array.isArray(item.labels) ? item.labels.join(',') : '', - String(item.sourceType ?? ''), - String(item.created ?? ''), - ], - } - ); - }, -}; - -function truncate(s: string, max: number): string { - return s.length > max ? s.slice(0, max - 1) + '…' : s; -} diff --git a/src/commands/memory/save.ts b/src/commands/memory/save.ts deleted file mode 100644 index 1d8b901..0000000 --- a/src/commands/memory/save.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace, requirePositional, getAllPositional, getArgString } from '../helpers'; - -export const memorySaveCommand: Command = { - name: 'memory save', - description: 'Save a memory', - operationId: 'memories.post', - positional: [{ name: 'content', description: 'Markdown content of the memory', variadic: true }], - options: [ - { flag: '--title ', description: 'Title (default: first line of content)', type: 'string' }, - { flag: '--labels ', description: 'Comma-separated labels', type: 'string' }, - { flag: '--source-type ', description: 'investigation | change_analysis | observation', type: 'string' }, - ], - examples: ['polylane memory save "The auth service uses Redis for sessions"'], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - requirePositional(args, 0, 'content'); - const content = getAllPositional(args).join(' '); - - let title = getArgString(args, 'title'); - if (!title) { - const firstLine = content.split('\n')[0] ?? content; - title = firstLine.length > 80 ? firstLine.slice(0, 77) + '…' : firstLine; - } - - const labelsRaw = getArgString(args, 'labels'); - const labels = labelsRaw ? labelsRaw.split(',').map((s) => s.trim()) : undefined; - const sourceType = getArgString(args, 'sourceType') as - | 'investigation' - | 'change_analysis' - | 'observation' - | undefined; - - const api = new PolylaneAPI(config); - const result = await api.memoriesPost({ - workspaceId, - title, - markdown: content, - ...(labels ? { labels } : {}), - ...(sourceType ? { sourceType } : {}), - }); - - formatOutput(config, { id: result.id, title: result.title }); - }, -}; diff --git a/src/commands/memory/show.ts b/src/commands/memory/show.ts deleted file mode 100644 index 36615ee..0000000 --- a/src/commands/memory/show.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace, requirePositional } from '../helpers'; - -export const memoryShowCommand: Command = { - name: 'memory show', - description: 'Show a memory with its markdown body', - operationId: 'memories.get', - positional: [{ name: 'memory-id', description: 'The memory ID' }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const id = requirePositional(args, 0, 'memory-id'); - const api = new PolylaneAPI(config); - const result = await api.memoriesGet(workspaceId, id); - formatOutput(config, result); - }, -}; diff --git a/src/commands/note/delete.ts b/src/commands/note/delete.ts deleted file mode 100644 index 2715381..0000000 --- a/src/commands/note/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { requireWorkspace, requirePositional, getArgBoolean } from '../helpers'; -import { promptConfirm } from '../../utils/prompt'; -import { isInteractive } from '../../utils/env'; - -export const noteDeleteCommand: Command = { - name: 'note delete', - description: 'Delete a daily note', - operationId: 'notes.del', - positional: [{ name: 'note-id', description: 'The daily note ID' }], - options: [{ flag: '--yes', description: 'Skip confirmation', type: 'boolean' }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const id = requirePositional(args, 0, 'note-id'); - - if (getArgBoolean(args, 'yes') !== true && isInteractive(config.nonInteractive)) { - const ok = await promptConfirm( - { nonInteractive: config.nonInteractive }, - `Delete note ${id}?`, - false - ); - if (!ok) { - process.stderr.write('Cancelled. Nothing changed.\n'); - return; - } - } - - const api = new PolylaneAPI(config); - const result = await api.notesDel(workspaceId, id); - process.stdout.write(`Deleted ${result.id}\n`); - }, -}; diff --git a/src/commands/note/global-clear.ts b/src/commands/note/global-clear.ts deleted file mode 100644 index 46eeaa8..0000000 --- a/src/commands/note/global-clear.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { requireWorkspace, getArgBoolean } from '../helpers'; -import { promptConfirm } from '../../utils/prompt'; -import { isInteractive } from '../../utils/env'; - -export const noteGlobalClearCommand: Command = { - name: 'note global clear', - description: 'Delete the workspace global note', - operationId: 'notes.global.del', - options: [{ flag: '--yes', description: 'Skip confirmation', type: 'boolean' }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - - if (getArgBoolean(args, 'yes') !== true && isInteractive(config.nonInteractive)) { - const ok = await promptConfirm( - { nonInteractive: config.nonInteractive }, - `Clear the workspace global note?`, - false - ); - if (!ok) { - process.stderr.write('Cancelled. Nothing changed.\n'); - return; - } - } - - const api = new PolylaneAPI(config); - await api.notesGlobalDel(workspaceId); - process.stdout.write(`Cleared global note\n`); - }, -}; diff --git a/src/commands/note/global-set.ts b/src/commands/note/global-set.ts deleted file mode 100644 index 5c6582b..0000000 --- a/src/commands/note/global-set.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace, requirePositional, getAllPositional } from '../helpers'; - -export const noteGlobalSetCommand: Command = { - name: 'note global set', - description: 'Replace the workspace global note', - operationId: 'notes.global.put', - positional: [{ name: 'content', description: 'Markdown content', variadic: true }], - examples: ['polylane note global set "Production runbook: …"'], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - requirePositional(args, 0, 'content'); - const content = getAllPositional(args).join(' '); - const api = new PolylaneAPI(config); - const result = await api.notesGlobalPut(workspaceId, { content }); - formatOutput(config, result); - }, -}; diff --git a/src/commands/note/global-show.ts b/src/commands/note/global-show.ts deleted file mode 100644 index af90abb..0000000 --- a/src/commands/note/global-show.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace } from '../helpers'; - -export const noteGlobalShowCommand: Command = { - name: 'note global show', - description: 'Show the workspace global note', - operationId: 'notes.global.get', - async execute(config: Config, _flags, _args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const api = new PolylaneAPI(config); - const result = await api.notesGlobalGet(workspaceId); - formatOutput(config, result); - }, -}; diff --git a/src/commands/note/index.ts b/src/commands/note/index.ts deleted file mode 100644 index 6196f47..0000000 --- a/src/commands/note/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { noteListCommand } from './list'; -import { noteShowCommand } from './show'; -import { noteSaveCommand } from './save'; -import { noteDeleteCommand } from './delete'; -import { noteGlobalShowCommand } from './global-show'; -import { noteGlobalSetCommand } from './global-set'; -import { noteGlobalClearCommand } from './global-clear'; - -export const noteCommands = [ - noteListCommand, - noteShowCommand, - noteSaveCommand, - noteDeleteCommand, - noteGlobalShowCommand, - noteGlobalSetCommand, - noteGlobalClearCommand, -]; diff --git a/src/commands/note/list.ts b/src/commands/note/list.ts deleted file mode 100644 index 81cb0e1..0000000 --- a/src/commands/note/list.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatList } from '../../output/formatter'; -import { projectItems } from '../../output/project'; -import { requireWorkspace, getArgNumber, getArgBoolean } from '../helpers'; - -const FIELDS = ['id', 'date', 'threadCount', 'updated']; - -export const noteListCommand: Command = { - name: 'note list', - description: 'List daily notes', - operationId: 'notes.list', - options: [ - { flag: '--limit ', description: 'Max items (default 20)', type: 'number' }, - { flag: '--full', description: 'Return full objects', type: 'boolean' }, - ], - examples: ['polylane note list', 'polylane note list --limit 7'], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const limit = getArgNumber(args, 'limit') ?? 20; - const full = getArgBoolean(args, 'full') === true; - - const api = new PolylaneAPI(config); - const result = await api.notesList(workspaceId, { perPage: limit }); - - if (full) { - formatList(config, result as { items: Array>; count: number }); - return; - } - - formatList( - config, - { - items: projectItems(result.items as unknown as Array>, FIELDS), - count: result.count, - }, - { - headers: ['ID', 'Date', 'Threads', 'Updated'], - rows: (item) => [ - String(item.id ?? ''), - String(item.date ?? ''), - String(item.threadCount ?? ''), - String(item.updated ?? ''), - ], - } - ); - }, -}; diff --git a/src/commands/note/save.ts b/src/commands/note/save.ts deleted file mode 100644 index 59cc552..0000000 --- a/src/commands/note/save.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace, requirePositional, getAllPositional, getArgString } from '../helpers'; -import { CLIError } from '../../errors/base'; -import { ExitCode } from '../../errors/codes'; - -export const noteSaveCommand: Command = { - name: 'note save', - description: 'Replace a daily note for a given date', - operationId: 'notes.put', - positional: [ - { name: 'note-id', description: 'The daily note ID' }, - { name: 'content', description: 'Markdown content', variadic: true }, - ], - options: [ - { flag: '--date ', description: 'Update the note date as well', type: 'string' }, - ], - examples: [ - 'polylane note save note_xxx "Deployed v0.2 to prod"', - ], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const id = requirePositional(args, 0, 'note-id'); - const contentParts = getAllPositional(args).slice(1); - if (contentParts.length === 0) { - throw new CLIError( - 'Missing ', - ExitCode.USAGE, - 'polylane note save ""' - ); - } - const content = contentParts.join(' '); - const date = getArgString(args, 'date'); - - const api = new PolylaneAPI(config); - const result = await api.notesPut(workspaceId, id, { - content, - ...(date ? { date } : {}), - }); - formatOutput(config, result); - }, -}; diff --git a/src/commands/note/show.ts b/src/commands/note/show.ts deleted file mode 100644 index 3e381ac..0000000 --- a/src/commands/note/show.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatOutput } from '../../output/formatter'; -import { requireWorkspace, requirePositional } from '../helpers'; - -export const noteShowCommand: Command = { - name: 'note show', - description: 'Show a daily note', - operationId: 'notes.get', - positional: [{ name: 'note-id', description: 'The daily note ID' }], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - const id = requirePositional(args, 0, 'note-id'); - const api = new PolylaneAPI(config); - const result = await api.notesGet(workspaceId, id); - formatOutput(config, result); - }, -}; diff --git a/src/commands/service/find.ts b/src/commands/service/find.ts deleted file mode 100644 index 05a4042..0000000 --- a/src/commands/service/find.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Command } from '../../command'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; -import { formatList } from '../../output/formatter'; -import { requireWorkspace, requirePositional, getAllPositional, getArgString } from '../helpers'; -import type { InfrastructureNode } from '../../generated/types'; - -export const serviceFindCommand: Command = { - name: 'service find', - description: 'Natural-language search over cloud infrastructure', - operationId: 'cloud_infra.nodes.search', - positional: [{ name: 'prompt', description: 'Search prompt', variadic: true }], - options: [ - { flag: '--provider

', description: 'Restrict to a provider', type: 'string' }, - { flag: '--type ', description: 'Restrict to a resource type', type: 'string' }, - ], - examples: ['polylane service find "lambda functions handling payments"'], - async execute(config: Config, _flags, args: Record): Promise { - const workspaceId = await requireWorkspace(config); - requirePositional(args, 0, 'prompt'); - const prompt = getAllPositional(args).join(' '); - - const api = new PolylaneAPI(config); - const provider = getArgString(args, 'provider'); - const type = getArgString(args, 'type'); - - const items = await api.cloudInfraNodesSearch( - { workspaceId, prompt }, - provider || type ? { provider, type } : undefined - ); - - const projected = items.map((n: InfrastructureNode) => ({ - id: `${n.provider}/${n.account}/${n.region}/${n.type}/${n.id}`, - type: n.type, - provider: n.provider, - account: n.account, - region: n.region, - })); - - formatList( - config, - { items: projected, count: projected.length }, - { - headers: ['ID', 'Type', 'Provider', 'Account', 'Region'], - rows: (item) => [ - String(item.id ?? ''), - String(item.type ?? ''), - String(item.provider ?? ''), - String(item.account ?? ''), - String(item.region ?? ''), - ], - } - ); - }, -}; diff --git a/src/commands/service/graph.ts b/src/commands/service/graph.ts index c58c1f1..f35fbbf 100644 --- a/src/commands/service/graph.ts +++ b/src/commands/service/graph.ts @@ -24,11 +24,11 @@ export const serviceGraphCommand: Command = { { flag: '--direction ', description: 'inbound|outbound|both (default both)', type: 'string' }, { flag: '--depth ', description: 'Traversal depth (default 1)', type: 'number' }, ], - examples: ['polylane service graph payments --direction outbound --depth 2'], + examples: ['polylane service graph aws/123456789/us-east-1/aws.lambda.function/payments --direction outbound --depth 2'], async execute(config: Config, _flags, args: Record): Promise { const workspaceId = await requireWorkspace(config); const raw = requirePositional(args, 0, 'service-id'); - const service = await resolveServiceId(config, raw, workspaceId); + const service = resolveServiceId(raw); const direction = (getArgString(args, 'direction') ?? 'both') as Direction; if (!['inbound', 'outbound', 'both'].includes(direction)) { diff --git a/src/commands/service/id.ts b/src/commands/service/id.ts index fc7a01c..47c9f28 100644 --- a/src/commands/service/id.ts +++ b/src/commands/service/id.ts @@ -1,7 +1,5 @@ import { CLIError } from '../../errors/base'; import { ExitCode } from '../../errors/codes'; -import type { Config } from '../../config/schema'; -import { PolylaneAPI } from '../../generated/client'; export type Provider = | 'aws' @@ -56,40 +54,14 @@ export function formatServiceId(node: { return `${node.provider}/${node.account}/${node.region}/${node.type}/${node.id}`; } -export async function resolveServiceId( - config: Config, - raw: string, - workspaceId: string -): Promise { +export function resolveServiceId(raw: string): ServiceId { const strict = parseServiceIdStrict(raw); if (strict) return strict; - const api = new PolylaneAPI(config); - const matches = await api.cloudInfraNodesSearch({ workspaceId, prompt: raw }); - - if (matches.length === 0) { - throw new CLIError( - `No service found matching "${raw}"`, - ExitCode.GENERAL, - 'Use the full form: ////' - ); - } - - if (matches.length === 1) { - const m = matches[0]!; - return { - provider: m.provider as Provider, - account: m.account, - region: m.region, - type: m.type, - id: m.id, - }; - } - - const topK = matches.slice(0, 5).map((m) => ` ${formatServiceId(m)}`).join('\n'); throw new CLIError( - `Multiple services match "${raw}"`, + `Invalid service ID "${raw}"`, ExitCode.USAGE, - `Candidates:\n${topK}\n\nUse the full form to disambiguate.` + 'Use the full form: ////\n' + + 'e.g. polylane service show aws/123456789/us-east-1/aws.lambda.function/payments' ); } diff --git a/src/commands/service/index.ts b/src/commands/service/index.ts index 954b7a2..209e3d9 100644 --- a/src/commands/service/index.ts +++ b/src/commands/service/index.ts @@ -1,6 +1,5 @@ import { serviceListCommand } from './list'; import { serviceShowCommand } from './show'; -import { serviceFindCommand } from './find'; import { serviceLogsCommand } from './logs'; import { serviceMetricsCommand } from './metrics'; import { serviceGraphCommand } from './graph'; @@ -8,7 +7,6 @@ import { serviceGraphCommand } from './graph'; export const serviceCommands = [ serviceListCommand, serviceShowCommand, - serviceFindCommand, serviceLogsCommand, serviceMetricsCommand, serviceGraphCommand, diff --git a/src/commands/service/logs.ts b/src/commands/service/logs.ts index a0f8aa2..950b334 100644 --- a/src/commands/service/logs.ts +++ b/src/commands/service/logs.ts @@ -19,14 +19,14 @@ export const serviceLogsCommand: Command = { { flag: '--templates', description: 'Recurring log patterns with historical rates instead of raw events', type: 'boolean' }, ], examples: [ - 'polylane service logs payments', - 'polylane service logs payments --since 24h --grep error', - 'polylane service logs payments --templates', + 'polylane service logs aws/123456789/us-east-1/aws.lambda.function/payments', + 'polylane service logs aws/123456789/us-east-1/aws.lambda.function/payments --since 24h --grep error', + 'polylane service logs aws/123456789/us-east-1/aws.lambda.function/payments --templates', ], async execute(config: Config, _flags, args: Record): Promise { const workspaceId = await requireWorkspace(config); const raw = requirePositional(args, 0, 'service-id'); - const service = await resolveServiceId(config, raw, workspaceId); + const service = resolveServiceId(raw); if (getArgBoolean(args, 'templates') === true) { await showTemplates(config, workspaceId, service); diff --git a/src/commands/service/metrics.ts b/src/commands/service/metrics.ts index 0bab3f7..c6190d5 100644 --- a/src/commands/service/metrics.ts +++ b/src/commands/service/metrics.ts @@ -13,11 +13,11 @@ export const serviceMetricsCommand: Command = { operationId: 'cloud_infra.nodes.metrics.summary', positional: [{ name: 'service-id', description: 'Service identifier' }], options: [{ flag: '--since ', description: 'Window (default 1h)', type: 'string' }], - examples: ['polylane service metrics payments --since 24h'], + examples: ['polylane service metrics aws/123456789/us-east-1/aws.lambda.function/payments --since 24h'], async execute(config: Config, _flags, args: Record): Promise { const workspaceId = await requireWorkspace(config); const raw = requirePositional(args, 0, 'service-id'); - const service = await resolveServiceId(config, raw, workspaceId); + const service = resolveServiceId(raw); const sinceStr = getArgString(args, 'since') ?? '1h'; const sinceMs = parseDuration(sinceStr); diff --git a/src/commands/service/show.ts b/src/commands/service/show.ts index 155a661..cf850a9 100644 --- a/src/commands/service/show.ts +++ b/src/commands/service/show.ts @@ -12,16 +12,15 @@ export const serviceShowCommand: Command = { description: 'Show a service with all its data', operationId: 'cloud_infra.nodes.get', positional: [ - { name: 'service-id', description: '//// or a short name' }, + { name: 'service-id', description: '////' }, ], examples: [ 'polylane service show aws/123456789/us-east-1/aws.lambda.function/payments', - 'polylane service show payments', ], async execute(config: Config, _flags, args: Record): Promise { const workspaceId = await requireWorkspace(config); const raw = requirePositional(args, 0, 'service-id'); - const service = await resolveServiceId(config, raw, workspaceId); + const service = resolveServiceId(raw); const api = new PolylaneAPI(config); const result = await api.cloudInfraNodesGet({ workspaceId, diff --git a/src/commands/thread/ask.ts b/src/commands/thread/ask.ts index 6c45b9f..2eb97e9 100644 --- a/src/commands/thread/ask.ts +++ b/src/commands/thread/ask.ts @@ -11,14 +11,12 @@ type ContextType = | 'repository' | 'cloud_account' | 'thread' - | 'infrastructure_node' - | 'memory'; + | 'infrastructure_node'; function inferContextType(id: string): ContextType { if (id.startsWith('repo_')) return 'repository'; if (id.startsWith('acc_')) return 'cloud_account'; if (id.startsWith('thrd_')) return 'thread'; - if (id.startsWith('mem_')) return 'memory'; return 'infrastructure_node'; } diff --git a/src/registry.ts b/src/registry.ts index b0eb185..3e87b71 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -15,13 +15,10 @@ export interface ResourceGroup { } const RESOURCE_ORDER: Record = { - feed: { name: 'feed', description: 'Workspace activity feed (what just happened)', order: 6 }, issue: { name: 'issue', description: 'Detected issues (anomalies + alerts) and their timelines', order: 8 }, service: { name: 'service', description: 'Cloud infrastructure (nodes, logs, metrics, graph)', order: 20 }, repo: { name: 'repo', description: 'Repositories', order: 30 }, tools: { name: 'tools', description: 'Run Polylane agent tools (observability, infra graph, GitHub reads)', order: 40 }, - memory: { name: 'memory', description: 'Persistent agent memory', order: 50 }, - note: { name: 'note', description: 'Daily and global workspace notes', order: 52 }, thread: { name: 'thread', description: 'Conversation threads', order: 60 }, artifact: { name: 'artifact', description: 'Per-thread artifacts (reports, diagrams, PRs)', order: 61 }, autofix: { name: 'autofix', description: 'Automated code fixes (read-only)', order: 66 }, diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 3cef9b6..dda6d70 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -51,12 +51,9 @@ describe('command resolution', () => { 'autofix', 'cloud', 'config', - 'feed', 'help', 'integration', 'issue', - 'memory', - 'note', 'repo', 'service', 'setup', @@ -87,10 +84,10 @@ describe('unknown command handling', () => { }) as typeof process.stdout.write; try { await assert.rejects( - helpCommand.execute(mockConfig(), {}, { _: ['nope', 'sub'] }), + helpCommand.execute(mockConfig(), {}, { _: ['servce', 'sub'] }), (err: unknown) => { assert.ok(err instanceof CLIError); - assert.equal(err.message, 'Unknown command: polylane nope sub. Closest match: polylane note.'); + assert.equal(err.message, 'Unknown command: polylane servce sub. Closest match: polylane service.'); assert.equal(err.exitCode, ExitCode.USAGE); assert.ok(err.hint?.includes('polylane --help')); return true; diff --git a/test/service-id.test.ts b/test/service-id.test.ts index 39dbf97..581adf1 100644 --- a/test/service-id.test.ts +++ b/test/service-id.test.ts @@ -1,6 +1,8 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { parseServiceIdStrict, formatServiceId } from '../src/commands/service/id'; +import { parseServiceIdStrict, formatServiceId, resolveServiceId } from '../src/commands/service/id'; +import { CLIError } from '../src/errors/base'; +import { ExitCode } from '../src/errors/codes'; describe('parseServiceIdStrict', () => { it('parses full form', () => { @@ -39,3 +41,27 @@ describe('formatServiceId', () => { assert.equal(formatServiceId(parsed), id); }); }); + +describe('resolveServiceId', () => { + it('accepts the full form', () => { + assert.deepEqual(resolveServiceId('aws/123456789/us-east-1/aws.lambda.function/payments'), { + provider: 'aws', + account: '123456789', + region: 'us-east-1', + type: 'aws.lambda.function', + id: 'payments', + }); + }); + + it('rejects a short name with the full form in the hint', () => { + assert.throws( + () => resolveServiceId('payments'), + (err: unknown) => { + assert.ok(err instanceof CLIError); + assert.equal(err.exitCode, ExitCode.USAGE); + assert.match(err.hint ?? '', /\/\/\/\//); + return true; + } + ); + }); +});