@dalmasonto/taskflow-mcp 1.0.35 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +138 -193
  2. package/dist/attachment-download.d.ts +74 -0
  3. package/dist/attachment-download.js +193 -0
  4. package/dist/attachment-download.js.map +1 -0
  5. package/dist/attachments.d.ts +23 -0
  6. package/dist/attachments.js +66 -0
  7. package/dist/attachments.js.map +1 -0
  8. package/dist/client.d.ts +206 -0
  9. package/dist/client.js +301 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/config.d.ts +137 -18
  12. package/dist/config.js +187 -106
  13. package/dist/config.js.map +1 -0
  14. package/dist/connect.d.ts +89 -0
  15. package/dist/connect.js +269 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/doctor.d.ts +24 -0
  18. package/dist/doctor.js +120 -0
  19. package/dist/doctor.js.map +1 -0
  20. package/dist/events.d.ts +208 -0
  21. package/dist/events.js +454 -0
  22. package/dist/events.js.map +1 -0
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.js +121 -218
  25. package/dist/index.js.map +1 -0
  26. package/dist/instructions.d.ts +12 -0
  27. package/dist/instructions.js +114 -0
  28. package/dist/instructions.js.map +1 -0
  29. package/dist/mint.d.ts +62 -0
  30. package/dist/mint.js +135 -0
  31. package/dist/mint.js.map +1 -0
  32. package/dist/mirror.d.ts +68 -0
  33. package/dist/mirror.js +103 -0
  34. package/dist/mirror.js.map +1 -0
  35. package/dist/pane-queue.d.ts +29 -0
  36. package/dist/pane-queue.js +35 -0
  37. package/dist/pane-queue.js.map +1 -0
  38. package/dist/prompts.d.ts +79 -0
  39. package/dist/prompts.js +211 -0
  40. package/dist/prompts.js.map +1 -0
  41. package/dist/resolve.d.ts +72 -0
  42. package/dist/resolve.js +89 -0
  43. package/dist/resolve.js.map +1 -0
  44. package/dist/runtime.d.ts +54 -0
  45. package/dist/runtime.js +339 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/server.d.ts +56 -0
  48. package/dist/server.js +793 -0
  49. package/dist/server.js.map +1 -0
  50. package/dist/session-identifier.d.ts +48 -0
  51. package/dist/session-identifier.js +44 -0
  52. package/dist/session-identifier.js.map +1 -0
  53. package/dist/sessions-store.d.ts +38 -0
  54. package/dist/sessions-store.js +88 -0
  55. package/dist/sessions-store.js.map +1 -0
  56. package/dist/tmux.d.ts +200 -0
  57. package/dist/tmux.js +580 -0
  58. package/dist/tmux.js.map +1 -0
  59. package/hooks/metadata.mjs +99 -0
  60. package/hooks/permission-prompt.mjs +100 -0
  61. package/hooks/taskflow-hook.mjs +499 -0
  62. package/hooks/tool-logging.mjs +63 -0
  63. package/package.json +38 -29
  64. package/dist/agent-registry.d.ts +0 -28
  65. package/dist/agent-registry.js +0 -158
  66. package/dist/db.d.ts +0 -5
  67. package/dist/db.js +0 -220
  68. package/dist/helpers.d.ts +0 -21
  69. package/dist/helpers.js +0 -27
  70. package/dist/resources.d.ts +0 -2
  71. package/dist/resources.js +0 -89
  72. package/dist/retry.d.ts +0 -34
  73. package/dist/retry.js +0 -94
  74. package/dist/sse.d.ts +0 -10
  75. package/dist/sse.js +0 -824
  76. package/dist/tmux-bridge.d.ts +0 -13
  77. package/dist/tmux-bridge.js +0 -217
  78. package/dist/tools/activity.d.ts +0 -39
  79. package/dist/tools/activity.js +0 -152
  80. package/dist/tools/agent-inbox.d.ts +0 -12
  81. package/dist/tools/agent-inbox.js +0 -272
  82. package/dist/tools/agent.d.ts +0 -14
  83. package/dist/tools/agent.js +0 -168
  84. package/dist/tools/analytics.d.ts +0 -21
  85. package/dist/tools/analytics.js +0 -191
  86. package/dist/tools/checkpoint.d.ts +0 -27
  87. package/dist/tools/checkpoint.js +0 -105
  88. package/dist/tools/notifications.d.ts +0 -31
  89. package/dist/tools/notifications.js +0 -59
  90. package/dist/tools/projects.d.ts +0 -55
  91. package/dist/tools/projects.js +0 -112
  92. package/dist/tools/settings.d.ts +0 -19
  93. package/dist/tools/settings.js +0 -73
  94. package/dist/tools/tasks.d.ts +0 -105
  95. package/dist/tools/tasks.js +0 -403
  96. package/dist/tools/terminal.d.ts +0 -4
  97. package/dist/tools/terminal.js +0 -98
  98. package/dist/tools/timer.d.ts +0 -37
  99. package/dist/tools/timer.js +0 -154
  100. package/dist/types.d.ts +0 -83
  101. package/dist/types.js +0 -30
package/dist/config.d.ts CHANGED
@@ -1,21 +1,140 @@
1
- export interface TaskFlowConfig {
2
- port: number;
3
- host: string;
4
- databasePath: string;
5
- logLevel: 'debug' | 'info' | 'warn' | 'error';
6
- agentLivenessInterval: number;
7
- maxPortAttempts: number;
8
- relayUrl: string;
9
- relayPushToken: string;
1
+ /**
2
+ * Load and validate `.taskflow.json`, then resolve one profile into a flat,
3
+ * ready-to-use identity for the HTTP client.
4
+ *
5
+ * The file is the stable per-repo credential a human creates via the dashboard's
6
+ * **API Base** page:
7
+ *
8
+ * ```json
9
+ * { "server": "http://localhost:8000", "project": 1, "default_profile": "main",
10
+ * "profiles": {
11
+ * "main": { "agent_id": 12, "key": "tfk_…", "display_name": "Builder" },
12
+ * "reviewer": { "agent_id": 13, "key": "tfk_…", "display_name": "Reviewer" }
13
+ * } }
14
+ * ```
15
+ *
16
+ * File resolution: an explicit path (or `TASKFLOW_CONFIG`) wins; otherwise we
17
+ * walk up from a start directory to the first `.taskflow.json`.
18
+ *
19
+ * Profile resolution (highest priority first):
20
+ * 1. an explicit `profile` argument (a per-tool-call override),
21
+ * 2. the `TASKFLOW_PROFILE` env var,
22
+ * 3. the file's `default_profile`,
23
+ * 4. the literal `"main"`.
24
+ *
25
+ * Every function that reads the environment or filesystem takes those as
26
+ * explicit, defaultable options so the whole module is unit-testable without
27
+ * touching real `process.env` or `process.cwd()`.
28
+ */
29
+ import { z } from "zod";
30
+ export declare const CONFIG_FILENAME = ".taskflow.json";
31
+ export declare const DEFAULT_PROFILE_NAME = "main";
32
+ /** One profile block as it appears in the file. */
33
+ export declare const profileSchema: z.ZodObject<{
34
+ agent_id: z.ZodNumber;
35
+ key: z.ZodString;
36
+ display_name: z.ZodOptional<z.ZodString>;
37
+ }, z.core.$strip>;
38
+ /** The whole `.taskflow.json` file. */
39
+ export declare const taskflowConfigSchema: z.ZodObject<{
40
+ server: z.ZodString;
41
+ project: z.ZodNumber;
42
+ default_profile: z.ZodOptional<z.ZodString>;
43
+ profiles: z.ZodRecord<z.ZodString, z.ZodObject<{
44
+ agent_id: z.ZodNumber;
45
+ key: z.ZodString;
46
+ display_name: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>>;
48
+ }, z.core.$strip>;
49
+ export type RawProfile = z.infer<typeof profileSchema>;
50
+ export type TaskflowConfig = z.infer<typeof taskflowConfigSchema>;
51
+ /** A profile flattened with its file-level context — everything the client needs. */
52
+ export interface ResolvedProfile {
53
+ server: string;
54
+ project: number;
55
+ profileName: string;
56
+ agentId: number;
57
+ key: string;
58
+ displayName: string;
59
+ /** Absolute path the config was read from (empty when parsed from a string). */
60
+ configPath: string;
61
+ }
62
+ /** A clear, user-facing configuration error (never a raw stack). */
63
+ export declare class ConfigError extends Error {
64
+ constructor(message: string);
65
+ }
66
+ export interface FindConfigOptions {
67
+ /** An explicit path to the file; skips the walk-up entirely. */
68
+ configPath?: string | undefined;
69
+ /** Where to start the upward walk (defaults to `process.cwd()`). */
70
+ startDir?: string | undefined;
71
+ /** Environment source (defaults to `process.env`). */
72
+ env?: NodeJS.ProcessEnv;
73
+ }
74
+ /**
75
+ * Resolve the absolute path of the `.taskflow.json` to use. Precedence:
76
+ * explicit `configPath` > `TASKFLOW_CONFIG` env > walk up from `startDir`.
77
+ * Throws {@link ConfigError} when nothing is found.
78
+ */
79
+ export declare function findConfigPath(options?: FindConfigOptions): string;
80
+ /** Validate an already-parsed object (or JSON text) into a {@link TaskflowConfig}. */
81
+ export declare function parseConfig(input: string | unknown, sourceLabel?: string): TaskflowConfig;
82
+ /** Read + validate the config file at an absolute path. */
83
+ export declare function loadConfigFile(configPath: string): TaskflowConfig;
84
+ export interface ResolveProfileOptions {
85
+ /** An explicit per-call profile override (highest precedence). */
86
+ profile?: string | undefined;
87
+ /** Environment source (defaults to `process.env`); reads `TASKFLOW_PROFILE`. */
88
+ env?: NodeJS.ProcessEnv;
89
+ /** Path the config came from, for error messages and the resolved result. */
90
+ configPath?: string;
91
+ }
92
+ /**
93
+ * Pick the effective profile name using the documented precedence:
94
+ * argument > `TASKFLOW_PROFILE` > `default_profile` > `"main"`.
95
+ */
96
+ export declare function chooseProfileName(config: TaskflowConfig, options?: ResolveProfileOptions): string;
97
+ /**
98
+ * Resolve `config` + options into a flat {@link ResolvedProfile}. Throws
99
+ * {@link ConfigError} if the chosen profile is not present in the file.
100
+ */
101
+ export declare function resolveProfile(config: TaskflowConfig, options?: ResolveProfileOptions): ResolvedProfile;
102
+ /** One selectable identity, as offered to the human choosing among them. */
103
+ export interface ProfileChoice {
104
+ name: string;
105
+ display_name: string;
106
+ /** True for the file's `default_profile` — a hint, never a decision. */
107
+ recommended: boolean;
108
+ /** Filled in by the caller from live session data; omitted if unknown. */
109
+ in_use?: boolean;
110
+ }
111
+ export type ProfileResolution = {
112
+ kind: "resolved";
113
+ profile: ResolvedProfile;
114
+ } | {
115
+ kind: "ambiguous";
116
+ profiles: ProfileChoice[];
117
+ };
118
+ export interface AskProfileOptions extends ResolveProfileOptions {
119
+ /** This terminal's remembered pick, if any (see `sessions-store.ts`). */
120
+ sticky?: string | undefined;
121
+ }
122
+ /**
123
+ * Resolve a profile, or report that a human has to choose.
124
+ *
125
+ * Ambiguous means: several profiles defined, and nothing said which one this
126
+ * terminal is. `default_profile` deliberately does NOT settle it — `--mint`
127
+ * adds profiles without moving it, so honouring it would mean the prompt never
128
+ * fires and two terminals silently share one identity, which is the bug.
129
+ *
130
+ * `resolveProfile` keeps its throwing contract for callers that already have a
131
+ * name (`--tmux --profile=x`, `--mint`, a per-tool `profile` argument).
132
+ */
133
+ export declare function resolveProfileOrAsk(config: TaskflowConfig, options?: AskProfileOptions): ProfileResolution;
134
+ export interface LoadProfileOptions extends FindConfigOptions, ResolveProfileOptions {
10
135
  }
11
- /** Keys that live in the config file (not SQLite) */
12
- export declare const SERVER_CONFIG_KEYS: Set<string>;
13
- export declare function getConfig(): TaskFlowConfig;
14
- /** Check if a setting key is a server-level config key */
15
- export declare function isServerConfigKey(key: string): boolean;
16
136
  /**
17
- * Update a single key in ~/.taskflow_config.json.
18
- * Re-reads the file first to avoid clobbering other keys.
19
- * Returns the updated config value.
137
+ * The one-call convenience the bin entry and hooks use: find the file, load +
138
+ * validate it, and resolve the chosen profile into a {@link ResolvedProfile}.
20
139
  */
21
- export declare function writeConfigKey(key: string, value: unknown): void;
140
+ export declare function loadProfile(options?: LoadProfileOptions): ResolvedProfile;
package/dist/config.js CHANGED
@@ -1,125 +1,206 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
- import { resolve, dirname } from 'path';
3
- import { homedir } from 'os';
4
- /** Keys that live in the config file (not SQLite) */
5
- export const SERVER_CONFIG_KEYS = new Set([
6
- 'port',
7
- 'host',
8
- 'databasePath',
9
- 'logLevel',
10
- 'agentLivenessInterval',
11
- 'maxPortAttempts',
12
- 'relayUrl',
13
- 'relayPushToken',
14
- ]);
15
- // ─── defaults ────────────────────────────────────────────────────────
16
- const DEFAULTS = {
17
- port: 3456,
18
- host: '127.0.0.1',
19
- databasePath: '~/.taskflow/taskflow.db',
20
- logLevel: 'info',
21
- agentLivenessInterval: 30_000,
22
- maxPortAttempts: 10,
23
- relayUrl: '',
24
- relayPushToken: '',
25
- };
26
- // ─── config file path ────────────────────────────────────────────────
27
- // Config lives alongside the database in ~/.taskflow/
28
- // Falls back to legacy ~/.taskflow_config.json for backward compat
29
- import { existsSync } from 'fs';
30
- const NEW_CONFIG_PATH = resolve(homedir(), '.taskflow', 'config.json');
31
- const LEGACY_CONFIG_PATH = resolve(homedir(), '.taskflow_config.json');
32
- const CONFIG_PATH = existsSync(NEW_CONFIG_PATH) ? NEW_CONFIG_PATH
33
- : existsSync(LEGACY_CONFIG_PATH) ? LEGACY_CONFIG_PATH
34
- : NEW_CONFIG_PATH; // default to new path for fresh installs
35
- // ─── loader ──────────────────────────────────────────────────────────
36
- function loadFromFile() {
37
- try {
38
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
39
- const parsed = JSON.parse(raw);
40
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
41
- console.error(`[config] Warning: ${CONFIG_PATH} is not a JSON object — using defaults`);
42
- return {};
1
+ /**
2
+ * Load and validate `.taskflow.json`, then resolve one profile into a flat,
3
+ * ready-to-use identity for the HTTP client.
4
+ *
5
+ * The file is the stable per-repo credential a human creates via the dashboard's
6
+ * **API Base** page:
7
+ *
8
+ * ```json
9
+ * { "server": "http://localhost:8000", "project": 1, "default_profile": "main",
10
+ * "profiles": {
11
+ * "main": { "agent_id": 12, "key": "tfk_…", "display_name": "Builder" },
12
+ * "reviewer": { "agent_id": 13, "key": "tfk_…", "display_name": "Reviewer" }
13
+ * } }
14
+ * ```
15
+ *
16
+ * File resolution: an explicit path (or `TASKFLOW_CONFIG`) wins; otherwise we
17
+ * walk up from a start directory to the first `.taskflow.json`.
18
+ *
19
+ * Profile resolution (highest priority first):
20
+ * 1. an explicit `profile` argument (a per-tool-call override),
21
+ * 2. the `TASKFLOW_PROFILE` env var,
22
+ * 3. the file's `default_profile`,
23
+ * 4. the literal `"main"`.
24
+ *
25
+ * Every function that reads the environment or filesystem takes those as
26
+ * explicit, defaultable options so the whole module is unit-testable without
27
+ * touching real `process.env` or `process.cwd()`.
28
+ */
29
+ import { readFileSync, existsSync } from "node:fs";
30
+ import { dirname, join, resolve } from "node:path";
31
+ import { z } from "zod";
32
+ export const CONFIG_FILENAME = ".taskflow.json";
33
+ export const DEFAULT_PROFILE_NAME = "main";
34
+ /** One profile block as it appears in the file. */
35
+ export const profileSchema = z.object({
36
+ agent_id: z.number().int(),
37
+ key: z.string().min(1, "profile key must not be empty"),
38
+ display_name: z.string().optional(),
39
+ });
40
+ /** The whole `.taskflow.json` file. */
41
+ export const taskflowConfigSchema = z.object({
42
+ server: z.string().min(1, "server must not be empty"),
43
+ project: z.number().int(),
44
+ default_profile: z.string().optional(),
45
+ profiles: z
46
+ .record(z.string(), profileSchema)
47
+ .refine((p) => Object.keys(p).length > 0, {
48
+ message: "profiles must contain at least one profile",
49
+ }),
50
+ });
51
+ /** A clear, user-facing configuration error (never a raw stack). */
52
+ export class ConfigError extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = "ConfigError";
56
+ }
57
+ }
58
+ /**
59
+ * Resolve the absolute path of the `.taskflow.json` to use. Precedence:
60
+ * explicit `configPath` > `TASKFLOW_CONFIG` env > walk up from `startDir`.
61
+ * Throws {@link ConfigError} when nothing is found.
62
+ */
63
+ export function findConfigPath(options = {}) {
64
+ const env = options.env ?? process.env;
65
+ const explicit = options.configPath ?? env.TASKFLOW_CONFIG;
66
+ if (explicit) {
67
+ const abs = resolve(explicit);
68
+ if (!existsSync(abs)) {
69
+ throw new ConfigError(`TaskFlow config not found at ${abs}`);
43
70
  }
44
- return parsed;
71
+ return abs;
45
72
  }
46
- catch (err) {
47
- if (err.code === 'ENOENT') {
48
- // File doesn't exist — that's fine, use defaults
49
- return {};
73
+ let dir = resolve(options.startDir ?? process.cwd());
74
+ // Walk up to the filesystem root looking for the file.
75
+ // eslint-disable-next-line no-constant-condition
76
+ while (true) {
77
+ const candidate = join(dir, CONFIG_FILENAME);
78
+ if (existsSync(candidate)) {
79
+ return candidate;
80
+ }
81
+ const parent = dirname(dir);
82
+ if (parent === dir) {
83
+ break;
50
84
  }
51
- console.error(`[config] Warning: failed to read ${CONFIG_PATH} — ${err.message}`);
52
- return {};
85
+ dir = parent;
53
86
  }
87
+ throw new ConfigError(`Could not find ${CONFIG_FILENAME} in ${resolve(options.startDir ?? process.cwd())} or any parent directory. ` +
88
+ `Create one (link an agent in the dashboard's API Base page) or set TASKFLOW_CONFIG.`);
54
89
  }
55
- function applyCliAndEnvOverrides(config) {
56
- // Env vars override config file
57
- if (process.env.TASKFLOW_SSE_PORT) {
58
- const p = parseInt(process.env.TASKFLOW_SSE_PORT, 10);
59
- if (!isNaN(p))
60
- config.port = p;
61
- }
62
- if (process.env.TASKFLOW_DB_PATH) {
63
- config.databasePath = process.env.TASKFLOW_DB_PATH;
64
- }
65
- if (process.env.TASKFLOW_HOST) {
66
- config.host = process.env.TASKFLOW_HOST;
67
- }
68
- if (process.env.TASKFLOW_LOG_LEVEL) {
69
- config.logLevel = process.env.TASKFLOW_LOG_LEVEL;
90
+ /** Validate an already-parsed object (or JSON text) into a {@link TaskflowConfig}. */
91
+ export function parseConfig(input, sourceLabel = "<string>") {
92
+ let data;
93
+ if (typeof input === "string") {
94
+ try {
95
+ data = JSON.parse(input);
96
+ }
97
+ catch (err) {
98
+ throw new ConfigError(`Failed to parse ${sourceLabel} as JSON: ${err.message}`);
99
+ }
70
100
  }
71
- if (process.env.TASKFLOW_RELAY_URL) {
72
- config.relayUrl = process.env.TASKFLOW_RELAY_URL;
101
+ else {
102
+ data = input;
73
103
  }
74
- if (process.env.TASKFLOW_RELAY_PUSH_TOKEN) {
75
- config.relayPushToken = process.env.TASKFLOW_RELAY_PUSH_TOKEN;
104
+ const result = taskflowConfigSchema.safeParse(data);
105
+ if (!result.success) {
106
+ const issues = result.error.issues
107
+ .map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`)
108
+ .join("\n");
109
+ throw new ConfigError(`Invalid ${sourceLabel}:\n${issues}`);
76
110
  }
77
- // CLI args take highest priority
78
- const portArgIdx = process.argv.indexOf('--port');
79
- if (portArgIdx !== -1 && process.argv[portArgIdx + 1]) {
80
- const p = parseInt(process.argv[portArgIdx + 1], 10);
81
- if (!isNaN(p))
82
- config.port = p;
111
+ return result.data;
112
+ }
113
+ /** Read + validate the config file at an absolute path. */
114
+ export function loadConfigFile(configPath) {
115
+ let text;
116
+ try {
117
+ text = readFileSync(configPath, "utf8");
83
118
  }
84
- const hostArgIdx = process.argv.indexOf('--host');
85
- if (hostArgIdx !== -1 && process.argv[hostArgIdx + 1]) {
86
- config.host = process.argv[hostArgIdx + 1];
119
+ catch (err) {
120
+ throw new ConfigError(`Failed to read ${configPath}: ${err.message}`);
87
121
  }
88
- return config;
122
+ return parseConfig(text, configPath);
89
123
  }
90
- // ─── singleton ───────────────────────────────────────────────────────
91
- let _config = null;
92
- export function getConfig() {
93
- if (_config)
94
- return _config;
95
- const fileValues = loadFromFile();
96
- _config = applyCliAndEnvOverrides({ ...DEFAULTS, ...fileValues });
97
- return _config;
124
+ /**
125
+ * Pick the effective profile name using the documented precedence:
126
+ * argument > `TASKFLOW_PROFILE` > `default_profile` > `"main"`.
127
+ */
128
+ export function chooseProfileName(config, options = {}) {
129
+ const env = options.env ?? process.env;
130
+ const fromArg = options.profile?.trim();
131
+ const fromEnv = env.TASKFLOW_PROFILE?.trim();
132
+ return ((fromArg && fromArg.length > 0 ? fromArg : undefined) ??
133
+ (fromEnv && fromEnv.length > 0 ? fromEnv : undefined) ??
134
+ config.default_profile ??
135
+ DEFAULT_PROFILE_NAME);
98
136
  }
99
- /** Check if a setting key is a server-level config key */
100
- export function isServerConfigKey(key) {
101
- return SERVER_CONFIG_KEYS.has(key);
137
+ /**
138
+ * Resolve `config` + options into a flat {@link ResolvedProfile}. Throws
139
+ * {@link ConfigError} if the chosen profile is not present in the file.
140
+ */
141
+ export function resolveProfile(config, options = {}) {
142
+ const profileName = chooseProfileName(config, options);
143
+ const profile = config.profiles[profileName];
144
+ if (!profile) {
145
+ const available = Object.keys(config.profiles).join(", ") || "(none)";
146
+ throw new ConfigError(`Profile "${profileName}" is not defined in ${options.configPath ?? CONFIG_FILENAME}. Available profiles: ${available}.`);
147
+ }
148
+ return {
149
+ server: config.server.replace(/\/+$/, ""),
150
+ project: config.project,
151
+ profileName,
152
+ agentId: profile.agent_id,
153
+ key: profile.key,
154
+ displayName: profile.display_name ?? profileName,
155
+ configPath: options.configPath ?? "",
156
+ };
102
157
  }
103
- // ─── write-back ──────────────────────────────────────────────────────
104
158
  /**
105
- * Update a single key in ~/.taskflow_config.json.
106
- * Re-reads the file first to avoid clobbering other keys.
107
- * Returns the updated config value.
159
+ * Resolve a profile, or report that a human has to choose.
160
+ *
161
+ * Ambiguous means: several profiles defined, and nothing said which one this
162
+ * terminal is. `default_profile` deliberately does NOT settle it — `--mint`
163
+ * adds profiles without moving it, so honouring it would mean the prompt never
164
+ * fires and two terminals silently share one identity, which is the bug.
165
+ *
166
+ * `resolveProfile` keeps its throwing contract for callers that already have a
167
+ * name (`--tmux --profile=x`, `--mint`, a per-tool `profile` argument).
108
168
  */
109
- export function writeConfigKey(key, value) {
110
- let existing = {};
111
- try {
112
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
113
- existing = JSON.parse(raw);
169
+ export function resolveProfileOrAsk(config, options = {}) {
170
+ const env = options.env ?? process.env;
171
+ const explicit = options.profile?.trim() || env.TASKFLOW_PROFILE?.trim();
172
+ if (explicit) {
173
+ // Unknown name is a real error, not an invitation to ask: the caller
174
+ // asserted an identity and got it wrong.
175
+ return { kind: "resolved", profile: resolveProfile(config, { ...options, profile: explicit }) };
114
176
  }
115
- catch {
116
- // File doesn't exist or is malformed start fresh
177
+ const names = Object.keys(config.profiles);
178
+ // A stale sticky pick (profile since removed from the file) falls through to
179
+ // asking rather than throwing — the human never typed that name today.
180
+ const sticky = options.sticky?.trim();
181
+ if (sticky && config.profiles[sticky]) {
182
+ return { kind: "resolved", profile: resolveProfile(config, { ...options, profile: sticky }) };
117
183
  }
118
- existing[key] = value;
119
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
120
- writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
121
- // Update the in-memory singleton so get_setting reflects the change immediately
122
- if (_config && key in DEFAULTS) {
123
- _config[key] = value;
184
+ if (names.length === 1) {
185
+ return { kind: "resolved", profile: resolveProfile(config, { ...options, profile: names[0] }) };
124
186
  }
187
+ const recommended = config.default_profile ?? DEFAULT_PROFILE_NAME;
188
+ return {
189
+ kind: "ambiguous",
190
+ profiles: names.map((name) => ({
191
+ name,
192
+ display_name: config.profiles[name]?.display_name ?? name,
193
+ recommended: name === recommended,
194
+ })),
195
+ };
196
+ }
197
+ /**
198
+ * The one-call convenience the bin entry and hooks use: find the file, load +
199
+ * validate it, and resolve the chosen profile into a {@link ResolvedProfile}.
200
+ */
201
+ export function loadProfile(options = {}) {
202
+ const configPath = findConfigPath(options);
203
+ const config = loadConfigFile(configPath);
204
+ return resolveProfile(config, { ...options, configPath });
125
205
  }
206
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The agent's connection to TaskFlow: register a session, prove liveness, and
3
+ * keep doing both for as long as the process lives.
4
+ *
5
+ * ## Why this is its own module
6
+ *
7
+ * This ran as step one of `startMirrorForThisAgent`'s `start(pane)` callback,
8
+ * behind `mirror.ts`'s "no tmux pane, nothing to mirror" early return. A
9
+ * capability (presence) was smuggled inside a feature (terminal mirroring), so
10
+ * the feature's precondition became the capability's: an agent outside tmux
11
+ * never registered, never heartbeat, and never appeared in the dashboard.
12
+ *
13
+ * Mirroring is now layered on top of this — `onSession` hands the caller a live
14
+ * session to attach the event stream and the pane mirror to.
15
+ *
16
+ * Retries are UNBOUNDED. The old startup gave up after 8 attempts (~2 minutes)
17
+ * and stayed dead for the life of the process, which is indistinguishable from
18
+ * the bug this module fixes: starting the MCP before the backend is a normal
19
+ * ordering, not an error.
20
+ */
21
+ import { TaskflowClient } from "./client.js";
22
+ import type { ResolvedProfile } from "./config.js";
23
+ export type ConnectionState = "starting" | "active" | "retrying" | "needs_profile" | "stopped";
24
+ export interface ConnectionStatus {
25
+ state: ConnectionState;
26
+ /** Why, in the caller's words — always set for `retrying` and `needs_profile`. */
27
+ detail?: string;
28
+ attempts: number;
29
+ /** The live session row id, once registered. */
30
+ session?: number;
31
+ /**
32
+ * WHICH profile this connection is for. Absent before a connection exists
33
+ * (`starting` / `needs_profile` after a reset).
34
+ *
35
+ * A session id on its own is not enough for a caller to reuse: a tool called
36
+ * with an explicit `profile:` other than the connected one would send another
37
+ * agent's session under this profile's credential, which the backend rejects.
38
+ */
39
+ profile?: string;
40
+ }
41
+ export interface ConnectedContext {
42
+ client: TaskflowClient;
43
+ session: number;
44
+ profile: ResolvedProfile;
45
+ pane: string | null;
46
+ }
47
+ export interface ConnectOptions {
48
+ profile: ResolvedProfile;
49
+ /** The tmux pane, or null outside tmux. Null is a normal, connectable state. */
50
+ pane: string | null;
51
+ /** Fired ONCE, on the first successful registration. */
52
+ onSession?: (ctx: ConnectedContext) => void | Promise<void>;
53
+ /** Test seam. */
54
+ createClient?: (profile: ResolvedProfile) => TaskflowClient;
55
+ log?: (line: string) => void;
56
+ sleep?: (ms: number) => Promise<void>;
57
+ heartbeatMs?: number;
58
+ /** Test seam. False drives heartbeats by hand via `handle.beat()`, so a
59
+ * background tick cannot race an assertion. Always true in production. */
60
+ autoHeartbeat?: boolean;
61
+ }
62
+ export interface ConnectionHandle {
63
+ stop: () => void;
64
+ /**
65
+ * Resolves when the initial registration settles — and NEVER rejects.
66
+ *
67
+ * "Settles" means registered, or stopped, or the injected sleep gave up. It
68
+ * does NOT mean "resolves eventually": retries are unbounded, so while the
69
+ * backend is down this stays PENDING forever. Callers on the startup path
70
+ * (Task 6) must therefore not `await` it — doing so would keep the MCP
71
+ * server from serving a single tool call until the backend came up. Start
72
+ * the connection, keep the handle, carry on; only tests await it, and they
73
+ * inject a `sleep` with a finite budget.
74
+ */
75
+ settled: Promise<void>;
76
+ /** Run one heartbeat tick now. Tests use it; production uses the loop. */
77
+ beat: () => Promise<void>;
78
+ }
79
+ /** The current connection status, for `whoami` to report. */
80
+ export declare function getConnectionStatus(): ConnectionStatus;
81
+ /** Record that we are deliberately NOT connecting until a human picks. */
82
+ export declare function setNeedsProfile(detail: string): void;
83
+ /** Test seam — reset module-level status between cases. */
84
+ export declare function resetConnectionStatus(): void;
85
+ /**
86
+ * Connect and stay connected. Never throws and never rejects: the agent must
87
+ * keep working whether or not the dashboard is reachable.
88
+ */
89
+ export declare function startConnection(options: ConnectOptions): ConnectionHandle;