@devosurf/tesser 0.1.0-alpha.2 → 0.1.0-alpha.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devosurf/tesser",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Tesser CLI — the machine-first interface for agents and humans.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "dependencies": {
20
20
  "commander": "^15.0.0",
21
21
  "esbuild": "^0.28.1",
22
- "@devosurf/tesser-sdk": "0.1.0-alpha.2",
23
- "@devosurf/tesser-testing": "0.1.0-alpha.2"
22
+ "@devosurf/tesser-testing": "0.1.0-alpha.4",
23
+ "@devosurf/tesser-sdk": "0.1.0-alpha.4"
24
24
  },
25
25
  "main": "./dist/index.js",
26
26
  "types": "./src/index.ts",
@@ -0,0 +1,39 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test, vi } from "vitest";
5
+ import type { ApiClient } from "../client.js";
6
+ import type { Output } from "../output.js";
7
+ import { doctor } from "./doctor.js";
8
+
9
+ const roots: string[] = [];
10
+
11
+ afterEach(() => {
12
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
+ });
14
+
15
+ function out(): Output & { data: ReturnType<typeof vi.fn> } {
16
+ return { data: vi.fn(), log: vi.fn(), fail: vi.fn(), json: true, format: "json" } as unknown as Output & { data: ReturnType<typeof vi.fn> };
17
+ }
18
+
19
+ describe("tesser doctor", () => {
20
+ test("reports local project checks without requiring network", async () => {
21
+ const root = mkdtempSync(join(tmpdir(), "tesser-doctor-"));
22
+ roots.push(root);
23
+ writeFileSync(join(root, "package.json"), JSON.stringify({ dependencies: { "@devosurf/tesser-sdk": "0.1.0" } }));
24
+ writeFileSync(join(root, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
25
+ const output = out();
26
+
27
+ await doctor(output, {
28
+ version: "0.1.0",
29
+ target: { url: "https://inst.example", token: undefined, project: "demo", projectRoot: root },
30
+ client: { get: vi.fn() } as unknown as ApiClient,
31
+ network: false,
32
+ });
33
+
34
+ const report = output.data.mock.calls[0]?.[0] as { project: string; checks: Array<{ name: string; ok: boolean }> };
35
+ expect(report.project).toBe("demo");
36
+ expect(report.checks.find((check) => check.name === "pnpm-lock")?.ok).toBe(true);
37
+ expect(report.checks.find((check) => check.name === "auth-token")?.ok).toBe(false);
38
+ });
39
+ });
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ApiClient } from "../client.js";
4
+ import type { ResolvedTarget } from "../config.js";
5
+ import type { Output } from "../output.js";
6
+
7
+ export interface DoctorCheck {
8
+ name: string;
9
+ ok: boolean;
10
+ severity: "info" | "warning" | "error";
11
+ message: string;
12
+ hint?: string;
13
+ }
14
+
15
+ export interface DoctorReport {
16
+ ok: boolean;
17
+ cliVersion: string;
18
+ node: string;
19
+ cwd: string;
20
+ project: string | null;
21
+ projectRoot: string | null;
22
+ instance: string;
23
+ auth: { tokenPresent: boolean };
24
+ checks: DoctorCheck[];
25
+ network?: { ok: boolean; health?: unknown; error?: string };
26
+ }
27
+
28
+ export async function doctor(
29
+ out: Output,
30
+ opts: { version: string; target: ResolvedTarget; client: ApiClient; network: boolean },
31
+ ): Promise<void> {
32
+ const checks: DoctorCheck[] = [];
33
+ checks.push({
34
+ name: "project",
35
+ ok: opts.target.projectRoot !== null,
36
+ severity: opts.target.projectRoot ? "info" : "warning",
37
+ message: opts.target.projectRoot ? `Project ${opts.target.project ?? "(unnamed)"} found` : "No tesser.json found from this directory",
38
+ ...(opts.target.projectRoot ? {} : { hint: "Run inside a Tesser Project, or create one with `tesser init <name>`." }),
39
+ });
40
+ checks.push({
41
+ name: "auth-token",
42
+ ok: opts.target.token !== undefined,
43
+ severity: opts.target.token ? "info" : "warning",
44
+ message: opts.target.token ? "API token is configured" : "No API token configured",
45
+ ...(opts.target.token ? {} : { hint: "Run `tesser login` or set TESSER_TOKEN." }),
46
+ });
47
+
48
+ if (opts.target.projectRoot) {
49
+ const pkgPath = join(opts.target.projectRoot, "package.json");
50
+ const lockPath = join(opts.target.projectRoot, "pnpm-lock.yaml");
51
+ checks.push({
52
+ name: "package-json",
53
+ ok: existsSync(pkgPath),
54
+ severity: existsSync(pkgPath) ? "info" : "warning",
55
+ message: existsSync(pkgPath) ? "package.json exists" : "package.json missing",
56
+ });
57
+ checks.push({
58
+ name: "pnpm-lock",
59
+ ok: existsSync(lockPath),
60
+ severity: existsSync(lockPath) ? "info" : "warning",
61
+ message: existsSync(lockPath) ? "pnpm-lock.yaml exists" : "pnpm-lock.yaml missing",
62
+ ...(existsSync(lockPath) ? {} : { hint: "Run `pnpm install` and commit pnpm-lock.yaml for reproducible Instance builds." }),
63
+ });
64
+ if (existsSync(pkgPath)) {
65
+ try {
66
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
67
+ const pinned = pkg.dependencies?.["@devosurf/tesser-sdk"] ?? pkg.devDependencies?.["@devosurf/tesser"];
68
+ checks.push({
69
+ name: "tesser-pins",
70
+ ok: typeof pinned === "string" && pinned.length > 0 && pinned !== "latest",
71
+ severity: typeof pinned === "string" && pinned.length > 0 && pinned !== "latest" ? "info" : "warning",
72
+ message: pinned ? `Tesser package pin detected (${pinned})` : "No Tesser package pin detected",
73
+ ...(pinned && pinned !== "latest" ? {} : { hint: "Run `tesser upgrade` with the target CLI version." }),
74
+ });
75
+ } catch (cause) {
76
+ checks.push({ name: "package-json-parse", ok: false, severity: "warning", message: `package.json is not valid JSON: ${String(cause)}` });
77
+ }
78
+ }
79
+ }
80
+
81
+ let network: DoctorReport["network"];
82
+ if (opts.network && opts.target.token) {
83
+ try {
84
+ network = { ok: true, health: await opts.client.get("/health") };
85
+ checks.push({ name: "instance-health", ok: true, severity: "info", message: `Instance reachable at ${opts.target.url}` });
86
+ } catch (cause) {
87
+ const error = cause instanceof Error ? cause.message : String(cause);
88
+ network = { ok: false, error };
89
+ checks.push({
90
+ name: "instance-health",
91
+ ok: false,
92
+ severity: "error",
93
+ message: error,
94
+ hint: "Check --url/TESSER_URL and the API token, or rerun with --no-network for local checks only.",
95
+ });
96
+ }
97
+ }
98
+
99
+ const report: DoctorReport = {
100
+ ok: checks.every((check) => check.severity !== "error" && check.ok),
101
+ cliVersion: opts.version,
102
+ node: process.version,
103
+ cwd: process.cwd(),
104
+ project: opts.target.project ?? null,
105
+ projectRoot: opts.target.projectRoot,
106
+ instance: opts.target.url,
107
+ auth: { tokenPresent: opts.target.token !== undefined },
108
+ checks,
109
+ ...(network !== undefined ? { network } : {}),
110
+ };
111
+
112
+ out.data(report, (r: DoctorReport) => {
113
+ const lines = [`Tesser CLI ${r.cliVersion}`, `instance: ${r.instance}`, `project: ${r.project ?? "(none)"}`];
114
+ for (const check of r.checks) lines.push(`${check.ok ? "ok" : check.severity}: ${check.name} — ${check.message}${check.hint ? ` (${check.hint})` : ""}`);
115
+ return lines.join("\n");
116
+ });
117
+ }
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, test, vi } from "vitest";
@@ -27,6 +27,15 @@ afterEach(() => {
27
27
  });
28
28
 
29
29
  describe("tesser init", () => {
30
+ test("refuses to overwrite a non-empty directory without --force", () => {
31
+ const parent = tempRoot();
32
+ const root = join(parent, "occupied");
33
+ mkdirSync(root);
34
+ writeFileSync(join(root, "package.json"), "{}\n");
35
+
36
+ expect(() => init(out(), "occupied", { dir: parent }, "0.2.0-beta.1")).toThrow("is not empty");
37
+ });
38
+
30
39
  test("pins Tesser packages and writes committed agent docs", () => {
31
40
  const parent = tempRoot();
32
41
  init(out(), "agent-docs", { dir: parent, instance: "https://tesser.example.com" }, "0.2.0-beta.1");
@@ -47,6 +56,10 @@ describe("tesser init", () => {
47
56
  expect(readFileSync(join(root, "AGENTS.md"), "utf8")).toContain("./.tesser/docs/README.md");
48
57
  expect(readFileSync(join(root, ".tesser", "docs", "sdk.md"), "utf8")).toContain("ctx.step");
49
58
  expect(readFileSync(join(root, ".tesser", "docs", "cli.md"), "utf8")).toContain("pnpm-lock.yaml");
59
+ const connectorsDoc = readFileSync(join(root, ".tesser", "docs", "connectors.md"), "utf8");
60
+ expect(connectorsDoc).toContain("node_modules/@devosurf/tesser-connectors/manifest.json");
61
+ expect(connectorsDoc).toContain("Do not guess Connector APIs");
62
+ expect(connectorsDoc).not.toContain("## Common Actions");
50
63
 
51
64
  const manifest = readJson<{ tesserVersion: string; project: string }>(join(root, ".tesser", "docs", "manifest.json"));
52
65
  expect(manifest).toMatchObject({ tesserVersion: "0.2.0-beta.1", project: "agent-docs" });
@@ -1,7 +1,7 @@
1
1
  // `tesser init <name>`: scaffold a Project — the linked repo unit (ADR-0006). One
2
2
  // automation per directory; colocated tests; git is the source of truth.
3
3
 
4
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { EXIT } from "../exit-codes.js";
7
7
  import { CliError, Output } from "../output.js";
@@ -33,13 +33,19 @@ test("greets by name", async () => {
33
33
  });
34
34
  `;
35
35
 
36
- export function init(out: Output, name: string, opts: { dir?: string | undefined; instance?: string | undefined }, version: string): void {
36
+ export function init(out: Output, name: string, opts: { dir?: string | undefined; instance?: string | undefined; force?: boolean | undefined }, version: string): void {
37
37
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
38
38
  throw new CliError(EXIT.USAGE, "project name must be kebab-case");
39
39
  }
40
40
  const root = join(opts.dir ?? process.cwd(), name);
41
- if (existsSync(join(root, "tesser.json"))) {
42
- throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
41
+ if (existsSync(root) && !opts.force) {
42
+ if (existsSync(join(root, "tesser.json"))) {
43
+ throw new CliError(EXIT.CONFLICT, `${root} is already a Tesser project`);
44
+ }
45
+ const entries = readdirSync(root).filter((entry) => entry !== ".DS_Store");
46
+ if (entries.length > 0) {
47
+ throw new CliError(EXIT.CONFLICT, `${root} is not empty — choose a new name or pass --force to write into it`);
48
+ }
43
49
  }
44
50
  mkdirSync(join(root, "automations", "hello"), { recursive: true });
45
51
 
@@ -143,7 +143,11 @@ function docsReadmeMd(projectName: string, version: string): string {
143
143
  }
144
144
 
145
145
  function cliReferenceMd(version: string): string {
146
- return `# Tesser CLI reference\n\nGenerated for \`@devosurf/tesser@${version}\`. The CLI is the agent interface: prefer \`--json\` when output will drive follow-up actions. stdout is data; stderr is logs/progress.\n\n## Install and invoke\n\n\`\`\`bash\nnpm install -g @devosurf/tesser@${version}\ntesser --version\n\n# Or run an exact version without global install:\nnpx @devosurf/tesser@${version} --version\n\`\`\`\n\n## Common flow\n\n\`\`\`bash\ntesser init my-project --instance https://tesser.example.com\ncd my-project\npnpm install # creates pnpm-lock.yaml; commit it\ngit init && git add -A && git commit -m init\ntesser login --instance https://tesser.example.com --token "$TESSER_TOKEN"\ntesser link --json # registers this Project on the Instance\ntesser test --json\ntesser build --json\ntesser deploy --json\n\`\`\`\n\n## Commands agents commonly use\n\n- \`tesser init <name> [--dir DIR] [--instance URL]\` — scaffold a Project.\n- \`tesser upgrade\` — pin Tesser packages to this CLI version and refresh \`.tesser/docs/\`. To target a version: \`npx @devosurf/tesser@<version> upgrade\`.\n- \`tesser login --instance URL --token TOKEN\` — verify and store a profile. Prefer \`TESSER_TOKEN\` over pasting tokens into chat.\n- \`tesser link [--repo URL] --json\` — register the Project and print deploy-key/webhook setup data. CLI output must not include private keys or webhook secrets.\n- \`tesser status --json\` — instance health and deploy state.\n- \`tesser test [--smoke-only] [--automation ID] --json\` — fast local tests plus generated smoke tests.\n- \`tesser build --json\` — bundle and statically extract manifests. Use before deploy when changing requirements.\n- \`tesser dev [--port N] [--no-watch]\` — local Instance with embedded Postgres under ignored \`.tesser/*\` state.\n- \`tesser deploy [--ref REF] [--local] [--no-wait] --json\` — server-side build/test/promotion. Exit code 4 means halted on missing credentials; run \`tesser connect\`.\n- \`tesser connect [--wait] [--status TOKEN] --json\` — mint or poll the human connect link. The human supplies credentials in the browser; the agent only sees readiness.\n- \`tesser secrets list --json\`, \`tesser secrets rm <name> --json\`, \`printenv MY_SECRET | tesser secrets set <name> --value-stdin --json\`. Never put secret values in argv.\n- \`tesser runs list|show|trigger|signal|cancel --json\`; \`tesser logs <runId> [--follow]\`; \`tesser replay <runId>\`.\n- \`tesser rollback <automation> --to <version> --json\` alias re-point; no rebuild.\n\n## Credential safety\n\n- Do not ask the user to paste secrets into chat. Use masked secret prompts where your harness supports them, or ask the human to run a command locally.\n- Never pass raw secret values as CLI arguments. Use environment variables, profiles, connect links, OAuth, or \`--value-stdin\`.\n- Treat connect links and status tokens as sensitive operational material.\n\n## Deploy hygiene\n\n- Run \`pnpm install\` after init/upgrade and commit \`pnpm-lock.yaml\`; the Instance installs from lockfiles for reproducible builds.\n- Do not commit \`node_modules/\`, \`.env\`, or local runtime state in \`.tesser/*\` outside \`.tesser/docs/\`.\n- Git is the source of truth. Commit and push before expecting a normal Instance deploy to see changes.\n`;
146
+ return `# Tesser CLI reference\n\nGenerated for \`@devosurf/tesser@${version}\`. The CLI is the agent interface: prefer \`--output json\` (or \`--json\`) when output will drive follow-up actions. stdout is data; stderr is logs/progress. \`tesser schema\` prints the machine-readable command contract without auth or network.\n\n## Install and invoke\n\n\`\`\`bash\nnpm install -g @devosurf/tesser@${version}\ntesser --version\n\n# Or run an exact version without global install:\nnpx @devosurf/tesser@${version} --version\n\`\`\`\n\n## Common flow\n\n\`\`\`bash\ntesser init my-project --instance https://tesser.example.com\ncd my-project\npnpm install # creates pnpm-lock.yaml; commit it\ngit init && git add -A && git commit -m init\nprintf '%s' "$TESSER_TOKEN" | tesser login --url https://tesser.example.com --token-stdin\ntesser link --json # registers this Project on the Instance\ntesser test --json\ntesser build --json\ntesser deploy --json\n\`\`\`\n\n## Commands agents commonly use\n\n- \`tesser init <name> [--dir DIR] [--instance URL] [--force]\` — scaffold a Project; refuses non-empty directories unless forced.\n- \`tesser upgrade\` — pin Tesser packages to this CLI version and refresh \`.tesser/docs/\`. To target a version: \`npx @devosurf/tesser@<version> upgrade\`.\n- \`printf '%s' "$TESSER_TOKEN" | tesser login --url URL --token-stdin\` — verify and store a profile. Humans may run \`tesser login\` and use masked prompts; avoid \`--token\` because argv can leak.\n- \`tesser link [--repo URL] --json\` — register the Project and print deploy-key/webhook setup data. CLI output must not include private keys or webhook secrets.\n- \`tesser status --json\` — instance health and deploy state.\n- \`tesser test [--smoke-only] [--automation ID] --json\` — fast local tests plus generated smoke tests.\n- \`tesser build --json\` — bundle and statically extract manifests. Use before deploy when changing requirements.\n- \`tesser dev [--port N] [--no-watch]\` — local Instance with embedded Postgres under ignored \`.tesser/*\` state.\n- \`tesser deploy [--ref REF] [--local] [--no-wait] --json\` — server-side build/test/promotion. Exit code 4 means halted on missing credentials; run \`tesser connect\`.\n- \`tesser connect [--wait] [--status TOKEN] --json\` — mint or poll the human connect link. The human supplies credentials in the browser; the agent only sees readiness.\n- \`tesser secrets list --json\`, \`tesser secrets rm <name> --json\`, \`printenv MY_SECRET | tesser secrets set <name> --value-stdin --json\`. Never put secret values in argv.\n- \`tesser schema --json\` — inspect commands, arguments, outputs, exit codes, and mutation markers.
147
+ - \`tesser doctor --json\` — local Project/auth/package preflight; use \`--no-network\` for offline checks.
148
+ - \`tesser completion bash|zsh\` — print shell completion scripts.
149
+ - \`tesser runs list [--limit N] [--offset N] [--fields id,status] --json\`; \`tesser runs trigger <automation> --input-file - --json\`; \`tesser runs signal <runId> <name> --payload-file - --json\`; \`tesser runs show|cancel --json\`; \`tesser logs <runId> [--follow] [--limit N] [--offset N] [-o ndjson]\`; \`tesser replay <runId>\`.
150
+ - \`tesser harness connect claude-code --connect CONNECT_URL\` and \`tesser harness connect pi --connect CONNECT_URL\` — preferred Harness credential namespace; legacy \`tesser auth ...\` aliases remain.\n- \`tesser rollback <automation> --to <version> --json\` — alias re-point; no rebuild.\n\n## Credential safety\n\n- Do not ask the user to paste secrets into chat. Use masked secret prompts where your harness supports them, or ask the human to run a command locally.\n- Never pass raw secret values as CLI arguments. Use masked prompts, environment variables, profiles, connect links, OAuth, \`--token-stdin\`, \`--value-stdin\`, or \`--*-file -\` stdin lanes.\n- Treat connect links and status tokens as sensitive operational material.\n\n## Deploy hygiene\n\n- Run \`pnpm install\` after init/upgrade and commit \`pnpm-lock.yaml\`; the Instance installs from lockfiles for reproducible builds.\n- Do not commit \`node_modules/\`, \`.env\`, or local runtime state in \`.tesser/*\` outside \`.tesser/docs/\`.\n- Git is the source of truth. Commit and push before expecting a normal Instance deploy to see changes.\n`;
147
151
  }
148
152
 
149
153
  function sdkReferenceMd(version: string): string {
@@ -151,5 +155,5 @@ function sdkReferenceMd(version: string): string {
151
155
  }
152
156
 
153
157
  function connectorsReferenceMd(version: string): string {
154
- return `# Tesser Connector reference\n\nGenerated for \`@devosurf/tesser-connectors@${version}\`. A Connector is a typed integration; a Connection is an authed runtime instance injected by the Credential broker.\n\n## Available connector imports\n\n\`\`\`ts\nimport {\n anthropic,\n claudeCode,\n github,\n gmail,\n googleCalendar,\n googleDocs,\n googleDrive,\n googleSheets,\n http,\n outlookMail,\n pi,\n resend,\n slack,\n} from "@devosurf/tesser-connectors";\n\`\`\`\n\nOnly import Connectors that the Automation declares in \`connections: { ... }\`.\n\n## Pattern\n\n\`\`\`ts\nimport { defineAutomation, onSchedule } from "@devosurf/tesser-sdk";\nimport { github, slack } from "@devosurf/tesser-connectors";\nimport { z } from "zod";\n\nexport default defineAutomation({\n id: "digest",\n trigger: onSchedule({ cron: "0 9 * * *", tz: "UTC" }),\n connections: { github, slack },\n output: z.object({ posted: z.boolean(), count: z.number() }),\n\n run: async (_input, ctx) => {\n const issues = await ctx.step("fetch-open-issues", () =>\n ctx.connections.github.issues.list({ state: "open", labels: ["bug"] }),\n );\n\n if (issues.length === 0) return { posted: false, count: 0 };\n\n await ctx.step("post-to-slack", () =>\n ctx.connections.slack.chat.postMessage({ channel: "#ops", text: \`\${issues.length} bugs\` }),\n );\n\n return { posted: true, count: issues.length };\n },\n});\n\`\`\`\n\n## Common Actions\n\n- \`ctx.connections.http.get({ url, headers?, query? })\` and \`ctx.connections.http.request({ method, url, headers?, query?, body?, bodyText? })\`. Generic writes are not retry-safe; still wrap them in a Step.\n- \`ctx.connections.github.issues.list({ repo?, state?, labels?, limit? })\`, \`issues.create({ repo, title, body?, labels? })\`, \`issues.comment({ repo, number, body })\`, \`repos.get({ repo })\`.\n- \`ctx.connections.slack.chat.postMessage({ channel, text, threadTs? })\` and related Slack chat/conversation/user Actions.\n- \`ctx.connections.resend.emails.send(...)\`, Gmail/Outlook Mail and Google Calendar/Docs/Drive/Sheets Actions, Anthropic model Actions, Claude Code/Pi Harness-related Connectors: inspect package types or examples before using.\n\n## Connector triggers\n\nSome Connectors expose typed triggers, e.g. GitHub issue triggers, Slack event triggers, Gmail/Outlook mailbox poll triggers. Use the Connector's \`triggers\` constructors; do not hand-roll webhook delivery unless no Connector exists.\n\n\`\`\`ts\ntrigger: github.triggers.issueOpened({ repo: "owner/repo" })\n\`\`\`\n\n## Safety rules\n\n- Connector Actions are not automatically durable. The Automation author must wrap every Action call in \`ctx.step\`.\n- The imported Connector is not a token-bearing client. The authed client exists only at \`ctx.connections.<name>\` inside \`run\`.\n- For bespoke APIs, combine \`http\` with declared \`secrets: { ... }\`; do not put API keys in source, tests, logs, CLI args, or committed env files.\n- If deploy halts on a missing Connection, surface \`tesser connect\` output to the human. The agent must not complete OAuth or paste raw secrets itself.\n`;
158
+ return "# Tesser Connector reference\n\nGenerated for `@devosurf/tesser-connectors@__VERSION__`. This file is intentionally a source map, not a duplicated API reference. The installed Connector package is the authoritative version-pinned reference. Do not rely on training data, memory, or hand-written summaries.\n\n## Hard rule\n\nDo not guess Connector APIs from memory, training data, old examples, or web search. After `pnpm install`, inspect the installed package in `node_modules/@devosurf/tesser-connectors`.\n\n## Source-of-truth order\n\n1. Project policy: `AGENTS.md` and `.tesser/docs/connectors.md`.\n2. Connector inventory: `node_modules/@devosurf/tesser-connectors/manifest.json`.\n3. Import names: `node_modules/@devosurf/tesser-connectors/index.ts`.\n4. Exact input/output schemas, samples, triggers, provider quirks: `node_modules/@devosurf/tesser-connectors/<connector-id>/index.ts`.\n5. Provider OAuth facts: `node_modules/@devosurf/tesser-connectors/providers/*.ts` and `catalog/index.ts`.\n\nNever edit `node_modules`. Read it, then edit the Automation under `automations/<id>/` and validate with `tesser test --json` / `tesser build --json`.\n\n## Quick inspection commands\n\nRun `pnpm install` first if `node_modules` is absent.\n\n```bash\n# Barrel export names: use these names in `import { ... } from \"@devosurf/tesser-connectors\"`.\nsed -n '1,180p' node_modules/@devosurf/tesser-connectors/index.ts\n\n# Installed connector inventory with actions/triggers.\nnode - <<'NODE'\nconst fs = require('node:fs');\nconst manifest = JSON.parse(fs.readFileSync('node_modules/@devosurf/tesser-connectors/manifest.json', 'utf8'));\nfor (const c of manifest.connectors) {\n console.log(\"\");\n console.log(`${c.id}: ${c.describe}`);\n const auth = Object.keys(c.auth ?? {});\n if (auth.length) console.log(` auth: ${auth.join(', ')}`);\n if (c.actions?.length) console.log(` actions: ${c.actions.map((a) => `${a.path} [${a.safety}${a.retrySafe ? ', retry-safe' : ''}]`).join(', ')}`);\n if (c.triggers?.length) console.log(` triggers: ${c.triggers.map((t) => `${t.id} [${t.strategy}]`).join(', ')}`);\n}\nNODE\n\n# Exact schemas for one Connector, including Zod input/output objects.\nsed -n '1,260p' node_modules/@devosurf/tesser-connectors/outlook-mail/index.ts\n```\n\n## Authoring pattern\n\n```ts\nimport { defineAutomation, onSchedule } from \"@devosurf/tesser-sdk\";\nimport { github, slack } from \"@devosurf/tesser-connectors\";\nimport { z } from \"zod\";\n\nexport default defineAutomation({\n id: \"digest\",\n trigger: onSchedule({ cron: \"0 9 * * *\", tz: \"UTC\" }),\n connections: { github, slack },\n output: z.object({ posted: z.boolean(), count: z.number() }),\n\n run: async (_input, ctx) => {\n const issues = await ctx.step(\"fetch-open-issues\", () =>\n ctx.connections.github.issues.list({ state: \"open\", labels: [\"bug\"] }),\n );\n\n if (issues.length === 0) return { posted: false, count: 0 };\n\n await ctx.step(\"post-to-slack\", () =>\n ctx.connections.slack.chat.postMessage({ channel: \"#ops\", text: `${issues.length} bugs` }),\n );\n\n return { posted: true, count: issues.length };\n },\n});\n```\n\n## Rules that matter more than the Connector API\n\n- A Connector import is not an authed client. It is a typed requirement declaration.\n- Runtime calls go through `ctx.connections.<connection-key>` inside `run`.\n- Every Connector Action call must be inside `ctx.step(...)`.\n- Declare raw credentials separately with `secrets: { name: secret({ describe: \"...\" }) }`.\n- The Credential broker injects values at runtime; agents must never read or print tokens/secrets.\n- Deploy halts on missing Connections or Secrets; a human completes the connect link.\n\n## Connector ids currently shipped in this package\n\nRead `manifest.json` for the authoritative list. Current ids include core HTTP/email/repo/chat/calendar/document/spreadsheet/model/harness Connectors such as `http`, `github`, `slack`, `resend`, `gmail`, `outlook-mail`, Google Connectors, and Harness/model Connectors. Do not hardcode this list in Automations or docs; inspect the installed manifest.\n".replaceAll("__VERSION__", version);
155
159
  }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { completionScript } from "./completion.js";
3
+
4
+
5
+ describe("completion scripts", () => {
6
+ test("prints bash and zsh scripts", () => {
7
+ expect(completionScript("bash")).toContain("complete -F _tesser_completion tesser");
8
+ expect(completionScript("zsh")).toContain("#compdef tesser");
9
+ });
10
+
11
+ test("rejects unknown shells", () => {
12
+ expect(() => completionScript("fish")).toThrow("completion shell must be bash or zsh");
13
+ });
14
+ });
@@ -0,0 +1,92 @@
1
+ import { EXIT } from "./exit-codes.js";
2
+ import { CliError } from "./output.js";
3
+
4
+ const COMMANDS = [
5
+ "schema",
6
+ "init",
7
+ "upgrade",
8
+ "login",
9
+ "link",
10
+ "status",
11
+ "test",
12
+ "build",
13
+ "dev",
14
+ "deploy",
15
+ "connect",
16
+ "auth",
17
+ "harness",
18
+ "secrets",
19
+ "runs",
20
+ "logs",
21
+ "replay",
22
+ "rollback",
23
+ "doctor",
24
+ "completion",
25
+ ];
26
+
27
+ const CHILDREN: Record<string, string[]> = {
28
+ auth: ["claude-code", "pi"],
29
+ harness: ["connect"],
30
+ "harness connect": ["claude-code", "pi"],
31
+ secrets: ["list", "set", "rm"],
32
+ runs: ["list", "show", "trigger", "signal", "cancel"],
33
+ };
34
+
35
+ export function completionScript(shell: string): string {
36
+ if (shell === "bash") return bashCompletion();
37
+ if (shell === "zsh") return zshCompletion();
38
+ throw new CliError(EXIT.USAGE, "completion shell must be bash or zsh");
39
+ }
40
+
41
+ function bashCompletion(): string {
42
+ const commands = COMMANDS.join(" ");
43
+ const cases = Object.entries(CHILDREN)
44
+ .map(([parent, children]) => ` "${parent}") COMPREPLY=( $(compgen -W "${children.join(" ")}" -- "$cur") ) ;;`)
45
+ .join("\n");
46
+ return `# bash completion for tesser
47
+ _tesser_completion() {
48
+ local cur prev words cword
49
+ _init_completion -n : || return
50
+ if [[ $cword -eq 1 ]]; then
51
+ COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )
52
+ return
53
+ fi
54
+ local parent="\${COMP_WORDS[1]}"
55
+ if [[ $cword -ge 3 ]]; then parent="\${COMP_WORDS[1]} \${COMP_WORDS[2]}"; fi
56
+ case "$parent" in
57
+ ${cases}
58
+ esac
59
+ }
60
+ complete -F _tesser_completion tesser
61
+ `;
62
+ }
63
+
64
+ function zshCompletion(): string {
65
+ const commandLines = COMMANDS.map((command) => ` '${command}: :->${command.replace(/ /g, "-")}'`).join(" \\\n");
66
+ return `#compdef tesser
67
+ _tesser() {
68
+ local -a commands
69
+ _arguments -C \
70
+ '(-h --help)'{-h,--help}'[display help]' \
71
+ '--json[machine output]' \
72
+ '(-o --output)'{-o,--output}'[output format]:format:(auto text json ndjson)' \
73
+ '1:command:->cmds' \
74
+ '*::arg:->args'
75
+ case $state in
76
+ cmds)
77
+ _values 'tesser commands' \
78
+ ${commandLines}
79
+ ;;
80
+ args)
81
+ case $words[1] in
82
+ secrets) _values 'secrets commands' list set rm ;;
83
+ runs) _values 'runs commands' list show trigger signal cancel ;;
84
+ auth) _values 'auth commands' claude-code pi ;;
85
+ harness) _values 'harness commands' connect ;;
86
+ esac
87
+ ;;
88
+ esac
89
+ }
90
+ _tesser
91
+ `;
92
+ }
package/src/config.ts CHANGED
@@ -20,6 +20,8 @@ export interface LinkManifest {
20
20
  instance?: string;
21
21
  }
22
22
 
23
+ export const DEFAULT_INSTANCE_URL = "http://localhost:8377";
24
+
23
25
  const CONFIG_PATH = join(
24
26
  process.env["TESSER_CONFIG_DIR"] ?? join(homedir(), ".config", "tesser"),
25
27
  "config.json",
@@ -79,7 +81,7 @@ export function resolveTarget(opts: { url?: string; token?: string; profile?: st
79
81
  manifest?.instance ??
80
82
  process.env["TESSER_URL"] ??
81
83
  profile.url ??
82
- "http://localhost:8377",
84
+ DEFAULT_INSTANCE_URL,
83
85
  token: opts.token ?? process.env["TESSER_TOKEN"] ?? profile.token,
84
86
  project: manifest?.project,
85
87
  projectRoot,